diff --git a/.eslintrc.json b/.eslintrc.json index fcf4081..d5e51ef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "cesium/node" + "extends": "cesium/node", + "rules": { + "no-var": "off" + } } diff --git a/CHANGES.md b/CHANGES.md index 320fed1..1b91b64 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,25 @@ 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) * 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) * Added ability to load alpha textures. [#124](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/124) diff --git a/README.md b/README.md index 82f4e67..ba7c420 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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. ## Getting Started diff --git a/gulpfile.js b/gulpfile.js index c512034..6d3a06a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -22,7 +22,14 @@ process.env.PATH += environmentSeparator + nodeBinaries; 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(); jasmine.loadConfigFile('specs/jasmine.json'); jasmine.addReporter(new JasmineSpecReporter({ @@ -32,10 +39,10 @@ gulp.task('test', function (done) { jasmine.onComplete(function (passed) { done(argv.failTaskOnError && !passed ? 1 : 0); }); -}); +} -gulp.task('test-watch', function () { - gulp.watch(specFiles).on('change', function () { +function testWatch() { + return gulp.watch(specFiles).on('change', function () { // We can't simply depend on the test task because Jasmine // does not like being run multiple times in the same process. try { @@ -46,9 +53,9 @@ gulp.task('test-watch', function () { console.log('Tests failed to execute.'); } }); -}); +} -gulp.task('coverage', function () { +async function coverage() { fsExtra.removeSync('coverage/server'); child_process.execSync('nyc' + ' --all' + @@ -57,13 +64,12 @@ gulp.task('coverage', function () { ' -x "specs/**" -x "coverage/**" -x "doc/**" -x "bin/**" -x "index.js" -x "gulpfile.js"' + ' node_modules/jasmine/bin/jasmine.js' + ' JASMINE_CONFIG_PATH=specs/jasmine.json', { - stdio: [process.stdin, process.stdout, process.stderr] - }); + stdio: [process.stdin, process.stdout, process.stderr] + }); open('coverage/lcov-report/index.html'); -}); +} - -gulp.task('cloc', function() { +function cloc() { var cmdLine; var clocPath = path.join('node_modules', 'cloc', 'lib', 'cloc'); @@ -99,4 +105,4 @@ gulp.task('cloc', function() { }); }); }); -}); +} diff --git a/lib/createGltf.js b/lib/createGltf.js index a5cdd39..f3ebc53 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -4,6 +4,7 @@ var getBufferPadded = require('./getBufferPadded'); var getDefaultMaterial = require('./loadMtl').getDefaultMaterial; var Texture = require('./Texture'); +var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; var WebGLConstants = Cesium.WebGLConstants; @@ -23,6 +24,9 @@ function createGltf(objData, options) { var materials = objData.materials; var name = objData.name; + // Split materials used by primitives with different types of attributes + materials = splitIncompatibleMaterials(nodes, materials, options); + var gltf = { accessors : [], asset : {}, @@ -133,10 +137,8 @@ function addBufferView(gltf, buffers, accessors, byteStride, target) { } function addBuffers(gltf, bufferState, name) { - // Positions and normals share the same byte stride so they can share the same bufferView - var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); - var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers); - addBufferView(gltf, positionsAndNormalsBuffers, positionsAndNormalsAccessors, 12, WebGLConstants.ARRAY_BUFFER); + addBufferView(gltf, bufferState.positionBuffers, bufferState.positionAccessors, 12, WebGLConstants.ARRAY_BUFFER); + addBufferView(gltf, bufferState.normalBuffers, bufferState.normalAccessors, 12, WebGLConstants.ARRAY_BUFFER); addBufferView(gltf, bufferState.uvBuffers, bufferState.uvAccessors, 8, WebGLConstants.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) { for (var name in material) { 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); var materialIndex = gltf.materials.length; if (options.unlit) { @@ -224,43 +251,94 @@ function addMaterial(gltf, material, options) { return materialIndex; } -function getMaterial(gltf, materials, materialName, options) { - if (!defined(materialName)) { - // Create a default material if the primitive does not specify one - materialName = 'default'; - } - - var i; - var material; +function getMaterialByName(materials, materialName) { var materialsLength = materials.length; - for (i = 0; i < materialsLength; ++i) { + for (var i = 0; i < materialsLength; ++i) { if (materials[i].name === materialName) { - material = materials[i]; - break; + return materials[i]; } } +} - if (!defined(material)) { - material = getDefaultMaterial(options); - material.name = materialName; - } - - var materialIndex; - materialsLength = gltf.materials.length; - for (i = 0; i < materialsLength; ++i) { - if (gltf.materials[i].name === materialName) { - materialIndex = i; - break; +function getMaterialIndex(materials, materialName) { + var materialsLength = materials.length; + for (var i = 0; i < materialsLength; ++i) { + if (materials[i].name === materialName) { + return i; } } +} + +function getOrCreateGltfMaterial(gltf, materials, materialName, options) { + var material = getMaterialByName(materials, materialName); + var materialIndex = getMaterialIndex(gltf.materials, materialName); if (!defined(materialIndex)) { - materialIndex = addMaterial(gltf, material, options); + materialIndex = addGltfMaterial(gltf, material, options); } 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) { var count = array.length / components; var minMax = array.getMinMax(components); @@ -305,68 +383,73 @@ function requiresUint32Indices(nodes) { var meshes = nodes[i].meshes; var meshesLength = meshes.length; for (var j = 0; j < meshesLength; ++j) { - // Reserve the 65535 index for primitive restart - var vertexCount = meshes[j].positions.length / 3; - if (vertexCount > 65534) { - return true; + var primitives = meshes[j].primitives; + var primitivesLength = primitives.length; + for (var k = 0; k < primitivesLength; ++k) { + // Reserve the 65535 index for primitive restart + var vertexCount = primitives[k].positions.length / 3; + if (vertexCount > 65534) { + return true; + } } } } return false; } -function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) { - var hasPositions = mesh.positions.length > 0; - var hasNormals = mesh.normals.length > 0; - var hasUVs = mesh.uvs.length > 0; +function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitive, index, options) { + var hasPositions = primitive.positions.length > 0; + var hasNormals = primitive.normals.length > 0; + var hasUVs = primitive.uvs.length > 0; - // Vertex attributes are shared by all primitives in the mesh var accessorIndex; var attributes = {}; if (hasPositions) { - accessorIndex = addVertexAttribute(gltf, mesh.positions, 3, mesh.name + '_positions'); + accessorIndex = addVertexAttribute(gltf, primitive.positions, 3, mesh.name + '_' + index + '_positions'); attributes.POSITION = accessorIndex; - bufferState.positionBuffers.push(mesh.positions.toFloatBuffer()); + bufferState.positionBuffers.push(primitive.positions.toFloatBuffer()); bufferState.positionAccessors.push(accessorIndex); } if (hasNormals) { - accessorIndex = addVertexAttribute(gltf, mesh.normals, 3, mesh.name + '_normals'); + accessorIndex = addVertexAttribute(gltf, primitive.normals, 3, mesh.name + '_' + index + '_normals'); attributes.NORMAL = accessorIndex; - bufferState.normalBuffers.push(mesh.normals.toFloatBuffer()); + bufferState.normalBuffers.push(primitive.normals.toFloatBuffer()); bufferState.normalAccessors.push(accessorIndex); } 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; - bufferState.uvBuffers.push(mesh.uvs.toFloatBuffer()); + bufferState.uvBuffers.push(primitive.uvs.toFloatBuffer()); bufferState.uvAccessors.push(accessorIndex); } - // Unload resources - mesh.positions = undefined; - mesh.normals = undefined; - mesh.uvs = undefined; + var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + index + '_indices'); + var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer(); + bufferState.indexBuffers.push(indexBuffer); + bufferState.indexAccessors.push(indexAccessorIndex); + // Unload resources + primitive.positions = undefined; + primitive.normals = undefined; + primitive.uvs = undefined; + primitive.indices = undefined; + + var materialIndex = getOrCreateGltfMaterial(gltf, materials, primitive.material, options); + + return { + attributes : attributes, + indices : indexAccessorIndex, + material : materialIndex, + 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) { - var primitive = primitives[i]; - var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices'); - var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer(); - bufferState.indexBuffers.push(indexBuffer); - bufferState.indexAccessors.push(indexAccessorIndex); - - primitive.indices = undefined; // Unload resources - - var materialIndex = getMaterial(gltf, materials, primitive.material, options); - - gltfPrimitives.push({ - attributes : attributes, - indices : indexAccessorIndex, - material : materialIndex, - mode : WebGLConstants.TRIANGLES - }); + gltfPrimitives.push(addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitives[i], i, options)); } var gltfMesh = { diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 46eb608..e8a30b6 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -81,19 +81,15 @@ function loadMtl(mtlPath, options) { materials.push(material); } - /** - * Removes texture options from texture name - * NOTE: assumes no spaces in texture name - * - * @param {String} name - * @returns {String} The clean texture name - */ - function cleanTextureName (name) { + function normalizeTexturePath(texturePath, mtlDirectory) { + // Removes texture options from texture name + // Assumes no spaces in texture name var re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/; - if (re.test(name)) { - return name.split(/\s+/).pop(); + if (re.test(texturePath)) { + texturePath = texturePath.split(/\s+/).pop(); } - return name; + texturePath = texturePath.replace(/\\/g, '/'); + return path.normalize(path.join(mtlDirectory, texturePath)); } function parseLine(line) { @@ -144,31 +140,31 @@ function loadMtl(mtlPath, options) { material.alpha = correctAlpha(1.0 - parseFloat(value)); } else if (/^map_Ka /i.test(line)) { 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)) { 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)) { 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)) { 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)) { 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)) { 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)) { 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]; 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)) { // 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.'); diff --git a/lib/loadObj.js b/lib/loadObj.js index f99b4c6..1103e88 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -8,17 +8,12 @@ var loadMtl = require('./loadMtl'); var outsideDirectory = require('./outsideDirectory'); var readLines = require('./readLines'); -var Cartesian2 = Cesium.Cartesian2; var Cartesian3 = Cesium.Cartesian3; var ComponentDatatype = Cesium.ComponentDatatype; +var CoplanarPolygonGeometryLibrary = Cesium.CoplanarPolygonGeometryLibrary; var defaultValue = Cesium.defaultValue; 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 Ray = Cesium.Ray; var RuntimeError = Cesium.RuntimeError; var WindingOrder = Cesium.WindingOrder; @@ -36,14 +31,14 @@ function Node() { function Mesh() { this.name = undefined; this.primitives = []; - this.positions = new ArrayStorage(ComponentDatatype.FLOAT); - this.normals = new ArrayStorage(ComponentDatatype.FLOAT); - this.uvs = new ArrayStorage(ComponentDatatype.FLOAT); } function Primitive() { this.material = undefined; this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT); + 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) @@ -76,7 +71,7 @@ function loadObj(objPath, options) { // All nodes seen in the obj 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 vertexCacheLimit = 1000000; var vertexCacheCount = 0; @@ -94,7 +89,10 @@ function loadObj(objPath, options) { var faceUvs = []; var faceNormals = []; - var vertexIndices = []; + function clearVertexCache() { + vertexCache = {}; + vertexCacheCount = 0; + } function getName(name) { return (name === '' ? undefined : name); @@ -112,36 +110,56 @@ function loadObj(objPath, options) { mesh.name = getName(name); node.meshes.push(mesh); addPrimitive(); - - // Clear the vertex cache for each new mesh - vertexCache = {}; - vertexCacheCount = 0; - vertexCount = 0; } function addPrimitive() { primitive = new Primitive(); primitive.material = activeMaterial; mesh.primitives.push(primitive); + + // Clear the vertex cache for each new primitive + clearVertexCache(); + vertexCount = 0; } - function useMaterial(name) { - var material = getName(name); - activeMaterial = material; - - // Look to see if this material has already been used by a primitive in the mesh + function reusePrimitive(callback) { var primitives = mesh.primitives; var primitivesLength = primitives.length; for (var i = 0; i < primitivesLength; ++i) { - if (primitives[i].material === material) { - primitive = primitives[i]; - return; + if (primitives[i].material === activeMaterial) { + if (!defined(callback) || callback(primitives[i])) { + primitive = primitives[i]; + clearVertexCache(); + vertexCount = primitive.positions.length / 3; + return; + } } } - // Add a new primitive with this material 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) { var i = parseInt(a); if (i < 0) { @@ -158,9 +176,9 @@ function loadObj(objPath, options) { var px = positions.get(pi + 0); var py = positions.get(pi + 1); var pz = positions.get(pi + 2); - mesh.positions.push(px); - mesh.positions.push(py); - mesh.positions.push(pz); + primitive.positions.push(px); + primitive.positions.push(py); + primitive.positions.push(pz); } // Normals @@ -169,9 +187,9 @@ function loadObj(objPath, options) { var nx = normals.get(ni + 0); var ny = normals.get(ni + 1); var nz = normals.get(ni + 2); - mesh.normals.push(nx); - mesh.normals.push(ny); - mesh.normals.push(nz); + primitive.normals.push(nx); + primitive.normals.push(ny); + primitive.normals.push(nz); } // UVs @@ -179,8 +197,8 @@ function loadObj(objPath, options) { var ui = getOffset(u, uvs, 2); var ux = uvs.get(ui + 0); var uy = uvs.get(ui + 1); - mesh.uvs.push(ux); - mesh.uvs.push(uy); + primitive.uvs.push(ux); + primitive.uvs.push(uy); } } @@ -195,114 +213,13 @@ function loadObj(objPath, options) { // may be some duplicate vertices. vertexCacheCount++; if (vertexCacheCount > vertexCacheLimit) { - vertexCacheCount = 0; - vertexCache = {}; + clearVertexCache(); } } return index; } - // Given a set of 3D points, project them onto whichever axis will produce the least distortion. - 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) { + function getPosition(index, result) { var pi = getOffset(index, positions, 3); var px = positions.get(pi + 0); var py = positions.get(pi + 1); @@ -310,7 +227,7 @@ function loadObj(objPath, options) { return Cartesian3.fromElements(px, py, pz, result); } - function get3DNormal(index, result) { + function getNormal(index, result) { var ni = getOffset(index, normals, 3); var nx = normals.get(ni + 0); var ny = normals.get(ni + 1); @@ -318,36 +235,28 @@ function loadObj(objPath, options) { 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 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 scratch4 = new Cartesian3(); var scratch5 = new Cartesian3(); - // Checks if winding order matches the given normal. - function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normal) { - var A = get3DPoint(positionIndex1, scratch1); - var B = get3DPoint(positionIndex2, scratch2); - var C = get3DPoint(positionIndex3, scratch3); + var scratchCenter = new Cartesian3(); + var scratchAxis1 = new Cartesian3(); + var scratchAxis2 = new Cartesian3(); + var scratchNormal = new Cartesian3(); + 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 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) { - var isWindingCorrect = true; - var faceNormal; + var i; + var isWindingCorrect; - // If normals are defined, find a face normal to use in winding order sanitization. - // 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); - } + checkPrimitive(uvs, normals); 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 index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]); var index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]); addTriangle(index1, index2, index3, isWindingCorrect); } else { // Triangulate if the face is not a triangle - var positions3D = []; + var points = scratchPoints; + var vertexIndices = scratchVertexIndices; + + points.length = 0; vertexIndices.length = 0; - var i; for (i = 0; i < vertices.length; ++i) { var index = addVertex(vertices[i], positions[i], uvs[i], normals[i]); vertexIndices.push(index); - - // Collect the vertex positions as 3D points - if (i === scratchPositions3D.length) { - scratchPositions3D.push(new Cartesian3()); + if (i === scratchPositions.length) { + scratchPositions.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=1; i < vertices.length-1; ++i) { - 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); - } + for (i = 0; i < indices.length - 2; i += 3) { + addTriangle(vertexIndices[indices[i]], vertexIndices[indices[i+1]], vertexIndices[indices[i+2]], isWindingCorrect); } } } @@ -502,7 +398,7 @@ function loadObj(objPath, options) { uvs = undefined; // 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; } -function finishLoading(nodes, mtlPaths, objPath, options) { +function finishLoading(nodes, mtlPaths, objPath, usesMaterials, options) { nodes = cleanNodes(nodes); if (nodes.length === 0) { 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)); return loadMtls(mtlPaths, objPath, options) .then(function(materials) { - assignDefaultMaterial(nodes, materials); + if (materials.length > 0 && !usesMaterials) { + assignDefaultMaterial(nodes, materials, usesMaterials); + } return { nodes : nodes, 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) { var objDirectory = path.dirname(objPath); var materials = []; @@ -550,8 +453,8 @@ function loadMtls(mtlPaths, objPath, options) { }); return Promise.map(mtlPaths, function(mtlPath) { - mtlPath = path.resolve(objDirectory, mtlPath); - var shallowPath = path.resolve(path.join(objDirectory, path.basename(mtlPath))); + mtlPath = normalizeMtlPath(mtlPath, objDirectory); + var shallowPath = path.join(objDirectory, path.basename(mtlPath)); if (options.secure && outsideDirectory(mtlPath, objDirectory)) { // 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.'); @@ -586,9 +489,6 @@ function loadMtls(mtlPaths, objPath, options) { } function assignDefaultMaterial(nodes, materials) { - if (materials.length === 0) { - return; - } var defaultMaterial = materials[0].name; var nodesLength = nodes.length; for (var i = 0; i < nodesLength; ++i) { @@ -609,10 +509,10 @@ function removeEmptyMeshes(meshes) { return meshes.filter(function(mesh) { // Remove empty primitives 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 - return (mesh.primitives.length > 0) && (mesh.positions.length > 0); + // Valid meshes must have at least one primitive + return (mesh.primitives.length > 0); }); } diff --git a/package.json b/package.json index 610b3b1..c6ecc68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obj2gltf", - "version": "2.2.0", + "version": "2.3.2", "description": "Convert OBJ model format to glTF", "license": "Apache-2.0", "contributors": [ @@ -26,26 +26,26 @@ "node": ">=4.0.0" }, "dependencies": { - "bluebird": "^3.5.1", - "cesium": "^1.39.0", - "fs-extra": "^4.0.2", - "jpeg-js": "^0.3.3", - "mime": "^2.0.3", - "pngjs": "^3.3.0", - "yargs": "^10.0.3" + "bluebird": "^3.5.2", + "cesium": "^1.50.0", + "fs-extra": "^7.0.0", + "jpeg-js": "^0.3.4", + "mime": "^2.3.1", + "pngjs": "^3.3.3", + "yargs": "^12.0.2" }, "devDependencies": { - "cloc": "^2.3.3", - "coveralls": "^3.0.0", - "eslint": "^4.4.1", - "eslint-config-cesium": "^2.0.1", - "gulp": "^3.9.1", - "jasmine": "^2.7.0", - "jasmine-spec-reporter": "^4.2.0", - "jsdoc": "^3.5.4", - "nyc": "^11.1.0", + "cloc": "^2.3.4", + "coveralls": "^3.0.2", + "eslint": "^5.6.1", + "eslint-config-cesium": "^6.0.0", + "gulp": "^4.0.0", + "jasmine": "^3.2.0", + "jasmine-spec-reporter": "^4.2.1", + "jsdoc": "^3.5.5", + "nyc": "^13.0.1", "open": "^0.0.5", - "requirejs": "^2.3.4" + "requirejs": "^2.3.6" }, "scripts": { "jsdoc": "jsdoc ./lib -R ./README.md -d doc", diff --git a/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.mtl b/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.mtl new file mode 100644 index 0000000..d81606d --- /dev/null +++ b/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.mtl @@ -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 diff --git a/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj b/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj new file mode 100644 index 0000000..2762b61 --- /dev/null +++ b/specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj @@ -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 diff --git a/specs/data/box-mixed-attributes-2/cesium.png b/specs/data/box-mixed-attributes-2/cesium.png new file mode 100644 index 0000000..3b8baee Binary files /dev/null and b/specs/data/box-mixed-attributes-2/cesium.png differ diff --git a/specs/data/box-mixed-attributes/box-mixed-attributes.mtl b/specs/data/box-mixed-attributes/box-mixed-attributes.mtl new file mode 100644 index 0000000..4f8d129 --- /dev/null +++ b/specs/data/box-mixed-attributes/box-mixed-attributes.mtl @@ -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 diff --git a/specs/data/box-mixed-attributes/box-mixed-attributes.obj b/specs/data/box-mixed-attributes/box-mixed-attributes.obj new file mode 100644 index 0000000..cb9055e --- /dev/null +++ b/specs/data/box-mixed-attributes/box-mixed-attributes.obj @@ -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 diff --git a/specs/data/box-texture-options/box-texture-options.mtl b/specs/data/box-texture-options/box-texture-options.mtl index 0d45c3b..3434533 100644 --- a/specs/data/box-texture-options/box-texture-options.mtl +++ b/specs/data/box-texture-options/box-texture-options.mtl @@ -1,19 +1,19 @@ -# Blender MTL File: 'box.blend' -# Material Count: 1 - -newmtl Material -Ns 96.078431 -Ka 0.200000 0.200000 0.200000 -Kd 0.640000 0.640000 0.640000 -Ks 0.500000 0.500000 0.500000 -Ke 0.100000 0.100000 0.100000 -Ni 1.000000 -d 0.900000 -Tr 0.100000 -map_Ka -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 ambient.gif -map_Ke -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 emission.jpg -map_Kd -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 diffuse.png -map_Ks -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 specular.jpeg -map_Ns -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 shininess.png -map_Bump -bm 0.2 bump.png -illum 2 +# Blender MTL File: 'box.blend' +# Material Count: 1 + +newmtl Material +Ns 96.078431 +Ka 0.200000 0.200000 0.200000 +Kd 0.640000 0.640000 0.640000 +Ks 0.500000 0.500000 0.500000 +Ke 0.100000 0.100000 0.100000 +Ni 1.000000 +d 0.900000 +Tr 0.100000 +map_Ka -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 ambient.gif +map_Ke -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 emission.jpg +map_Kd -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 diffuse.png +map_Ks -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 specular.jpeg +map_Ns -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 shininess.png +map_Bump -bm 0.2 bump.png +illum 2 diff --git a/specs/data/box-texture-options/box-texture-options.obj b/specs/data/box-texture-options/box-texture-options.obj index b8d1dde..3207f00 100644 --- a/specs/data/box-texture-options/box-texture-options.obj +++ b/specs/data/box-texture-options/box-texture-options.obj @@ -1,46 +1,46 @@ -# Blender v2.78 (sub 0) OBJ File: 'box.blend' -# www.blender.org -mtllib box-texture-options.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 +# Blender v2.78 (sub 0) OBJ File: 'box.blend' +# www.blender.org +mtllib box-texture-options.mtl +o Cube +v -1.000000 -1.000000 1.000000 +v -1.000000 1.000000 1.000000 +v -1.000000 -1.000000 -1.000000 +v -1.000000 1.000000 -1.000000 +v 1.000000 -1.000000 1.000000 +v 1.000000 1.000000 1.000000 +v 1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 -1.000000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 0.0000 +vt 0.0000 1.0000 +vn -1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +vn 1.0000 0.0000 0.0000 +vn 0.0000 0.0000 1.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +usemtl Material +s off +f 1/1/1 2/2/1 4/3/1 3/4/1 +f 3/5/2 4/6/2 8/7/2 7/8/2 +f 7/9/3 8/10/3 6/11/3 5/12/3 +f 5/13/4 6/14/4 2/15/4 1/16/4 +f 3/5/5 7/17/5 5/18/5 1/16/5 +f 8/19/6 4/6/6 2/15/6 6/20/6 diff --git a/specs/data/box-windows-paths/box-windows-paths.obj b/specs/data/box-windows-paths/box-windows-paths.obj new file mode 100644 index 0000000..1c37d5b --- /dev/null +++ b/specs/data/box-windows-paths/box-windows-paths.obj @@ -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 diff --git a/specs/data/box-windows-paths/materials/box-windows-paths.mtl b/specs/data/box-windows-paths/materials/box-windows-paths.mtl new file mode 100644 index 0000000..22bca2d --- /dev/null +++ b/specs/data/box-windows-paths/materials/box-windows-paths.mtl @@ -0,0 +1,13 @@ +# Blender MTL File: 'box.blend' +# Material Count: 1 + +newmtl Material +Ns 96.078431 +Ka 0.000000 0.000000 0.000000 +Kd 0.640000 0.640000 0.640000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 +map_Kd .\images\cesium.png diff --git a/specs/data/box-windows-paths/materials/images/cesium.png b/specs/data/box-windows-paths/materials/images/cesium.png new file mode 100644 index 0000000..3b8baee Binary files /dev/null and b/specs/data/box-windows-paths/materials/images/cesium.png differ diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 1cde9e5..60f476a 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -4,14 +4,17 @@ var Promise = require('bluebird'); var obj2gltf = require('../../lib/obj2gltf'); var createGltf = require('../../lib/createGltf'); var loadObj = require('../../lib/loadObj'); +var getDefaultMaterial = require('../../lib/loadMtl').getDefaultMaterial; var clone = Cesium.clone; +var defined = Cesium.defined; var WebGLConstants = Cesium.WebGLConstants; var boxObjPath = 'specs/data/box/box.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 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; @@ -20,6 +23,7 @@ describe('createGltf', function() { var groupObjData; var complexObjData; var noMaterialsObjData; + var mixedAttributesObjData; beforeEach(function(done) { options = clone(obj2gltf.defaults); @@ -42,6 +46,10 @@ describe('createGltf', function() { loadObj(noMaterialsObjPath, options) .then(function(data) { noMaterialsObjData = data; + }), + loadObj(mixedAttributesObjPath, options) + .then(function(data) { + mixedAttributesObjData = data; }) ]).then(done); }); @@ -133,42 +141,108 @@ describe('createGltf', 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 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.NORMAL).toBeUndefined(); expect(attributes.TEXCOORD_0).toBeDefined(); }); 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 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.NORMAL).toBeDefined(); expect(attributes.TEXCOORD_0).toBeUndefined(); }); it('runs without uvs and normals', function() { - boxObjData.nodes[0].meshes[0].normals.length = 0; - boxObjData.nodes[0].meshes[0].uvs.length = 0; + boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0; + boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0; 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.NORMAL).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) { - var mesh = objData.nodes[0].meshes[0]; - var indices = mesh.primitives[0].indices; - var positions = mesh.positions; - var normals = mesh.normals; - var uvs = mesh.uvs; + var primitive = objData.nodes[0].meshes[0].primitives[0]; + var indices = primitive.indices; + var positions = primitive.positions; + var normals = primitive.normals; + var uvs = primitive.uvs; var indicesLength = indices.length; var vertexCount = positions.length / 3; @@ -192,12 +266,12 @@ describe('createGltf', function() { it('detects need to use uint32 indices', function() { expandObjData(boxObjData, 2731); // Right above 65536 limit - var mesh = boxObjData.nodes[0].meshes[0]; - var indicesLength = mesh.primitives[0].indices.length; - var vertexCount = mesh.positions.length / 3; + var primitive = boxObjData.nodes[0].meshes[0].primitives[0]; + var indicesLength = primitive.indices.length; + var vertexCount = primitive.positions.length / 3; 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]; expect(indicesAccessor.count).toBe(indicesLength); expect(indicesAccessor.max[0]).toBe(vertexCount - 1); diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index f1a04fb..cd0b7cd 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -34,9 +34,11 @@ var objExternalResourcesInRootPath = 'specs/data/box-external-resources-in-root/ var objTexturedPath = 'specs/data/box-textured/box-textured.obj'; var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.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 objConcavePath = 'specs/data/concave/concave.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'; function getMeshes(data) { @@ -93,9 +95,9 @@ describe('loadObj', function() { expect(node.name).toBe('Cube'); expect(mesh.name).toBe('Cube-Mesh'); - expect(mesh.positions.length / 3).toBe(24); - expect(mesh.normals.length / 3).toBe(24); - expect(mesh.uvs.length / 2).toBe(24); + expect(primitive.positions.length / 3).toBe(24); + expect(primitive.normals.length / 3).toBe(24); + expect(primitive.uvs.length / 2).toBe(24); expect(primitive.indices.length).toBe(36); expect(primitive.material).toBe('Material'); }), done).toResolve(); @@ -104,10 +106,10 @@ describe('loadObj', function() { it('loads obj with normals', function(done) { expect(loadObj(objNormalsPath, options) .then(function(data) { - var mesh = getMeshes(data)[0]; - expect(mesh.positions.length / 3).toBe(24); - expect(mesh.normals.length / 3).toBe(24); - expect(mesh.uvs.length / 2).toBe(0); + var primitive = getPrimitives(data)[0]; + expect(primitive.positions.length / 3).toBe(24); + expect(primitive.normals.length / 3).toBe(24); + expect(primitive.uvs.length / 2).toBe(0); }), done).toResolve(); }); @@ -115,8 +117,8 @@ describe('loadObj', function() { expect(loadObj(objUnnormalizedPath, options) .then(function(data) { var scratchNormal = new Cesium.Cartesian3(); - var mesh = getMeshes(data)[0]; - var normals = mesh.normals; + var primitive = getPrimitives(data)[0]; + var normals = primitive.normals; var normalsLength = normals.length / 3; for (var i = 0; i < normalsLength; ++i) { var normalX = normals.get(i * 3); @@ -131,10 +133,10 @@ describe('loadObj', function() { it('loads obj with uvs', function(done) { expect(loadObj(objUvsPath, options) .then(function(data) { - var mesh = getMeshes(data)[0]; - expect(mesh.positions.length / 3).toBe(20); - expect(mesh.normals.length / 3).toBe(0); - expect(mesh.uvs.length / 2).toBe(20); + var primitive = getPrimitives(data)[0]; + expect(primitive.positions.length / 3).toBe(20); + expect(primitive.normals.length / 3).toBe(0); + expect(primitive.uvs.length / 2).toBe(20); }), done).toResolve(); }); @@ -144,8 +146,8 @@ describe('loadObj', function() { loadObj(objNegativeIndicesPath, options) ]) .then(function(results) { - var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer(); - var positions = getMeshes(results[1])[0].positions.toFloatBuffer(); + var positionsReference = getPrimitives(results[0])[0].positions.toFloatBuffer(); + var positions = getPrimitives(results[1])[0].positions.toFloatBuffer(); expect(positions).toEqual(positionsReference); }), done).toResolve(); }); @@ -153,9 +155,8 @@ describe('loadObj', function() { it('loads obj with triangle faces', function(done) { expect(loadObj(objTrianglesPath, options) .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(primitive.indices.length).toBe(36); }), done).toResolve(); }); @@ -255,9 +256,8 @@ describe('loadObj', function() { it('loads obj with concave face containing 5 vertices', function(done) { expect(loadObj(objConcavePath, options) .then(function(data) { - var mesh = getMeshes(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); }), done).toResolve(); }); @@ -309,6 +309,13 @@ describe('loadObj', function() { expect(primitives[0].material).toBe('Red'); expect(primitives[1].material).toBe('Green'); 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(); }); @@ -478,6 +485,27 @@ describe('loadObj', function() { }), 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) { expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError); });