Merge pull request #153 from AnalyticalGraphicsInc/fix-uv-error

Remove faces that don't match the same attribute layout
This commit is contained in:
Matthew Amato 2018-09-19 15:34:13 -04:00 committed by GitHub
commit 6554214010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 103 deletions

View File

@ -1,13 +1,14 @@
Change Log
==========
### 2.3.0 ???
### 2.3.0 2018-??-??
* Fixed normalization on Windows paths running the converter on Linux. [#150](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/150)
* Fixed handling of objs with mismatching attribute layouts. [#153](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/153)
* Fixed normalization oo 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)

View File

@ -131,10 +131,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);
@ -297,68 +295,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 = getMaterial(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 = {

View File

@ -36,14 +36,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 +76,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;
@ -112,17 +112,17 @@ 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
vertexCache = {};
vertexCacheCount = 0;
vertexCount = 0;
}
function useMaterial(name) {
@ -158,9 +158,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 +169,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 +179,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);
}
}
@ -373,6 +373,17 @@ function loadObj(objPath, options) {
var isWindingCorrect = true;
var faceNormal;
var firstFace = primitive.indices.length === 0;
var faceHasUvs = uvs[0].length > 0;
var faceHasNormals = normals[0].length > 0;
var primitiveHasUvs = primitive.uvs.length > 0;
var primitiveHasNormals = primitive.normals.length > 0;
if (!firstFace && (faceHasUvs !== primitiveHasUvs || faceHasNormals !== primitiveHasNormals)) {
// Discard faces that don't use the same attributes
return;
}
// 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) {
@ -502,7 +513,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 +534,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 +542,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,
@ -591,9 +604,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) {
@ -614,10 +624,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);
});
}

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

@ -133,42 +133,42 @@ 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();
});
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 +192,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);

View File

@ -38,6 +38,7 @@ 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) {
@ -94,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();
@ -105,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();
});
@ -116,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);
@ -132,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();
});
@ -145,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();
});
@ -154,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();
});
@ -256,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();
});
@ -488,6 +487,14 @@ describe('loadObj', function() {
}), done).toResolve();
});
it('discards faces that don\'t use the same attributes as other faces in the primitive', function(done) {
expect(loadObj(objMixedAttributesPath, options)
.then(function(data) {
var primitive = getPrimitives(data)[0];
expect(primitive.indices.length).toBe(18); // 3 faces removed
}), done).toResolve();
});
it('throws when file has invalid contents', function(done) {
expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError);
});