diff --git a/CHANGES.md b/CHANGES.md index 02cdb37..8e7473d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,8 @@ Change Log ========== ### 2.3.2 ????-??-?? - +* 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 diff --git a/lib/createGltf.js b/lib/createGltf.js index b5f3f89..06a573d 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 : {}, @@ -70,14 +74,14 @@ function createGltf(objData, options) { var meshIndex; if (meshesLength === 1) { - meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, meshes[0], options); + meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, meshes[0]); addNode(gltf, node.name, meshIndex, undefined); } else { // Add meshes as child nodes var parentIndex = addNode(gltf, node.name); for (var j = 0; j < meshesLength; ++j) { var mesh = meshes[j]; - meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, mesh, options); + meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, mesh); addNode(gltf, mesh.name, meshIndex, parentIndex); } } @@ -194,6 +198,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)) { @@ -207,50 +236,101 @@ function resolveTextures(gltf, material) { } } -function addMaterial(gltf, material) { +function addGltfMaterial(gltf, material) { resolveTextures(gltf, material); var materialIndex = gltf.materials.length; gltf.materials.push(material); 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) { + var material = getMaterialByName(materials, materialName); + var materialIndex = getMaterialIndex(gltf.materials, materialName); if (!defined(materialIndex)) { - materialIndex = addMaterial(gltf, material); + materialIndex = addGltfMaterial(gltf, material); } 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); @@ -309,7 +389,7 @@ function requiresUint32Indices(nodes) { return false; } -function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitive, index, options) { +function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitive, index) { var hasPositions = primitive.positions.length > 0; var hasNormals = primitive.normals.length > 0; var hasUVs = primitive.uvs.length > 0; @@ -346,7 +426,7 @@ function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primiti primitive.uvs = undefined; primitive.indices = undefined; - var materialIndex = getMaterial(gltf, materials, primitive.material, options); + var materialIndex = getOrCreateGltfMaterial(gltf, materials, primitive.material); return { attributes : attributes, @@ -356,12 +436,12 @@ function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primiti }; } -function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) { +function addMesh(gltf, materials, bufferState, uint32Indices, mesh) { 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)); + gltfPrimitives.push(addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitives[i], i)); } var gltfMesh = { 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/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 1c4818a..06eeb58 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); }); @@ -163,6 +171,72 @@ describe('createGltf', function() { 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 primitive = objData.nodes[0].meshes[0].primitives[0]; var indices = primitive.indices;