diff --git a/lib/loadObj.js b/lib/loadObj.js index 148ff319..eb5131b1 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -9,12 +9,20 @@ var loadMtl = require('./loadMtl'); var readLines = require('./readLines'); var Axis = Cesium.Axis; +var Cartesian2 = Cesium.Cartesian2; var Cartesian3 = Cesium.Cartesian3; var ComponentDatatype = Cesium.ComponentDatatype; var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; +var IntersectionTests = Cesium.IntersectionTests; +var Matrix3 = Cesium.Matrix3; var Matrix4 = Cesium.Matrix4; +var OrientedBoundingBox = Cesium.OrientedBoundingBox; +var Plane = Cesium.Plane; +var PolygonPipeline = Cesium.PolygonPipeline; +var Ray = Cesium.Ray; var RuntimeError = Cesium.RuntimeError; +var WindingOrder = Cesium.WindingOrder; module.exports = loadObj; @@ -41,13 +49,10 @@ function Primitive() { } // OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js) -var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // v float float float -var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vn float float float -var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vt float float -var facePattern1 = /f( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)?\/?/; // f vertex vertex vertex ... -var facePattern2 = /f( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)?/; // f vertex/uv vertex/uv vertex/uv ... -var facePattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/; // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... -var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/; // f vertex//normal vertex//normal vertex//normal ... +var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // v float float float +var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vn float float float +var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vt float float +var facePattern = /(-?\d+)\/?(-?\d*)\/?(-?\d*)/g; // for any face format "f v", "f v/v", "f v//v", "f v/v/v" var scratchCartesian = new Cartesian3(); @@ -91,6 +96,17 @@ function loadObj(objPath, options) { // All mtl paths seen in the obj var mtlPaths = []; + // Buffers for face data that spans multiple lines + var lineBuffer = ''; + + // Used for parsing face data + var faceVertices = []; + var facePositions = []; + var faceUvs = []; + var faceNormals = []; + + var vertexIndices = []; + function getName(name) { return (name === '' ? undefined : name); } @@ -146,7 +162,7 @@ function loadObj(objPath, options) { function createVertex(p, u, n) { // Positions - if (defined(p)) { + if (p.length > 0) { var pi = getOffset(p, positions, 3); var px = positions.get(pi + 0); var py = positions.get(pi + 1); @@ -157,7 +173,7 @@ function loadObj(objPath, options) { } // Normals - if (defined(n)) { + if (n.length > 0) { var ni = getOffset(n, normals, 3); var nx = normals.get(ni + 0); var ny = normals.get(ni + 1); @@ -168,7 +184,7 @@ function loadObj(objPath, options) { } // UVs - if (defined(u)) { + if (u.length > 0) { var ui = getOffset(u, uvs, 2); var ux = uvs.get(ui + 0); var uy = uvs.get(ui + 1); @@ -195,21 +211,225 @@ function loadObj(objPath, options) { return index; } - function addFace(v1, p1, u1, n1, v2, p2, u2, n2, v3, p3, u3, n3, v4, p4, u4, n4) { - var index1 = addVertex(v1, p1, u1, n1); - var index2 = addVertex(v2, p2, u2, n2); - var index3 = addVertex(v3, p3, u3, n3); + // 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); + } - primitive.indices.push(index1); - primitive.indices.push(index2); - primitive.indices.push(index3); + Plane.fromPointNormal(scratchOrigin, scratchNormal, scratchPlane); + scratchRay.direction = scratchNormal; - // Triangulate if the face is a quad - if (defined(v4)) { - var index4 = addVertex(v4, p4, u4, n4); + for (i = 0; i < positions.length; i++) { + scratchRay.origin = positions[i]; + + var intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint); + + if (!defined(intersectionPoint)) { + Cartesian3.negate(scratchRay.direction, scratchRay.direction); + intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint); + } + var v = Cartesian3.subtract(intersectionPoint, scratchOrigin, intersectionPoint); + var x = Cartesian3.dot(planeXAxis, v); + var y = Cartesian3.dot(planeYAxis, v); + + if (i === scratchPositions2D.length) { + scratchPositions2D.push(new Cartesian2()); + } + + positions2D[i] = new Cartesian2.fromElements(x, y, scratchPositions2D[i]); + } + + return positions2D; + } + + function get3DPoint(index, result) { + var pi = getOffset(index, positions, 3); + var px = positions.get(pi + 0); + var py = positions.get(pi + 1); + var pz = positions.get(pi + 2); + return Cartesian3.fromElements(px, py, pz, result); + } + + function get3DNormal(index, result) { + var ni = getOffset(index, normals, 3); + var nx = normals.get(ni + 0); + var ny = normals.get(ni + 1); + var nz = normals.get(ni + 2); + 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 BA = Cartesian3.subtract(B, A, scratch4); + var CA = Cartesian3.subtract(C, A, scratch5); + var cross = Cartesian3.cross(BA, CA, scratch3); + + return (Cartesian3.dot(normal, cross) >= 0); + } + + function addTriangle(index1, index2, index3, correctWinding) { + if (correctWinding) { + primitive.indices.push(index1); + primitive.indices.push(index2); + primitive.indices.push(index3); + } else { primitive.indices.push(index1); primitive.indices.push(index3); - primitive.indices.push(index4); + primitive.indices.push(index2); + } + } + + var scratchPositions3D = [new Cartesian3(), new Cartesian3(), new Cartesian3()]; + function addFace(vertices, positions, uvs, normals) { + var isWindingCorrect = true; + var faceNormal; + + // 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); + } + + if (vertices.length === 3) { + 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 = []; + 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()); + } + positions3D.push(get3DPoint(positions[i], scratchPositions3D[i])); + } + + var positions2D = projectTo2D(positions3D); + + 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); + } + } } } @@ -256,34 +476,29 @@ function loadObj(objPath, options) { } else if ((result = uvPattern.exec(line)) !== null) { uvs.push(parseFloat(result[1])); uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image - } else if ((result = facePattern1.exec(line)) !== null) { - addFace( - result[1], result[1], undefined, undefined, - result[2], result[2], undefined, undefined, - result[3], result[3], undefined, undefined, - result[4], result[4], undefined, undefined - ); - } else if ((result = facePattern2.exec(line)) !== null) { - addFace( - result[1], result[2], result[3], undefined, - result[4], result[5], result[6], undefined, - result[7], result[8], result[9], undefined, - result[10], result[11], result[12], undefined - ); - } else if ((result = facePattern3.exec(line)) !== null) { - addFace( - result[1], result[2], result[3], result[4], - result[5], result[6], result[7], result[8], - result[9], result[10], result[11], result[12], - result[13], result[14], result[15], result[16] - ); - } else if ((result = facePattern4.exec(line)) !== null) { - addFace( - result[1], result[2], undefined, result[3], - result[4], result[5], undefined, result[6], - result[7], result[8], undefined, result[9], - result[10], result[11], undefined, result[12] - ); + } else { // face line or invalid line + // Because face lines can contain n vertices, we use a line buffer in case the face data spans multiple lines. + // If there's a line continuation don't create face yet + if (line.slice(-1) === '\\') { + lineBuffer += line.substring(0, line.length-1); + return; + } + lineBuffer += line; + if (lineBuffer.substring(0, 2) === 'f ') { + while ((result = facePattern.exec(lineBuffer)) !== null) { + faceVertices.push(result[0]); + facePositions.push(result[1]); + faceUvs.push(result[2]); + faceNormals.push(result[3]); + } + addFace(faceVertices, facePositions, faceUvs, faceNormals); + + faceVertices.length = 0; + facePositions.length = 0; + faceNormals.length = 0; + faceUvs.length = 0; + } + lineBuffer = ''; } } diff --git a/specs/data/box-triangles/box-triangles.mtl b/specs/data/box-triangles/box-triangles.mtl index abbc294b..70d3ba1d 100644 --- a/specs/data/box-triangles/box-triangles.mtl +++ b/specs/data/box-triangles/box-triangles.mtl @@ -1,12 +1,10 @@ -# Blender MTL File: 'box.blend' +# Blender MTL File: 'None' # Material Count: 1 -newmtl Material -Ns 96.078431 +newmtl None +Ns 0 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 +Kd 0.8 0.8 0.8 +Ks 0.8 0.8 0.8 +d 1 illum 2 diff --git a/specs/data/box-triangles/box-triangles.obj b/specs/data/box-triangles/box-triangles.obj index 124ab2de..b97f0d4b 100644 --- a/specs/data/box-triangles/box-triangles.obj +++ b/specs/data/box-triangles/box-triangles.obj @@ -1,7 +1,7 @@ -# Blender v2.78 (sub 0) OBJ File: 'box.blend' +# Blender v2.78 (sub 0) OBJ File: '' # www.blender.org mtllib box-triangles.mtl -o Cube +o Cube_Cube.001 v -1.000000 -1.000000 1.000000 v -1.000000 1.000000 1.000000 v -1.000000 -1.000000 -1.000000 @@ -10,37 +10,23 @@ 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 +usemtl None 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 +f 2//1 3//1 1//1 +f 4//2 7//2 3//2 +f 8//3 5//3 7//3 +f 6//4 1//4 5//4 +f 7//5 1//5 3//5 +f 4//6 6//6 8//6 +f 2//1 4//1 3//1 +f 4//2 8//2 7//2 +f 8//3 6//3 5//3 +f 6//4 2//4 1//4 +f 7//5 5//5 1//5 +f 4//6 2//6 6//6 diff --git a/specs/data/concave/concave.mtl b/specs/data/concave/concave.mtl new file mode 100644 index 00000000..70d3ba1d --- /dev/null +++ b/specs/data/concave/concave.mtl @@ -0,0 +1,10 @@ +# Blender MTL File: 'None' +# Material Count: 1 + +newmtl None +Ns 0 +Ka 0.000000 0.000000 0.000000 +Kd 0.8 0.8 0.8 +Ks 0.8 0.8 0.8 +d 1 +illum 2 diff --git a/specs/data/concave/concave.obj b/specs/data/concave/concave.obj new file mode 100644 index 00000000..aeb002d0 --- /dev/null +++ b/specs/data/concave/concave.obj @@ -0,0 +1,30 @@ +# Blender v2.78 (sub 0) OBJ File: '' +# www.blender.org +mtllib concave.mtl +o Plane +v -1.458150 0.363522 1.000000 +v 0.541850 0.363522 1.000000 +v -1.458150 0.363522 -1.000000 +v 0.541850 0.363522 -1.000000 +v -0.336510 0.363522 0.000000 +v -1.458150 -0.363522 1.000000 +v 0.541850 -0.363522 1.000000 +v -1.458150 -0.363522 -1.000000 +v 0.541850 -0.363522 -1.000000 +v -0.336510 -0.363522 0.000000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.7513 0.0000 -0.6599 +vn 0.7513 0.0000 0.6599 +vn -1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +vn 0.0000 0.0000 1.0000 +usemtl None +s off +f 1//1 2//1 5//1 4//1 3//1 +f 6//2 8//2 9//2 10//2 7//2 +f 2//3 7//3 10//3 5//3 +f 5//4 10//4 9//4 4//4 +f 3//5 8//5 6//5 1//5 +f 4//6 9//6 8//6 3//6 +f 1//7 6//7 7//7 2//7 diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index ca7d2a73..a43930da 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -19,6 +19,7 @@ var objTrianglesUrl = 'specs/data/box-triangles/box-triangles.obj'; var objObjectsUrl = 'specs/data/box-objects/box-objects.obj'; var objGroupsUrl = 'specs/data/box-groups/box-groups.obj'; var objObjectsGroupsUrl = 'specs/data/box-objects-groups/box-objects-groups.obj'; +var objConcaveUrl = 'specs/data/concave/concave.obj'; var objUsemtlUrl = 'specs/data/box-usemtl/box-usemtl.obj'; var objNoMaterialsUrl = 'specs/data/box-no-materials/box-no-materials.obj'; var objMultipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.obj'; @@ -196,6 +197,16 @@ describe('loadObj', function() { }), done).toResolve(); }); + it('loads obj with concave face containing 5 vertices', function(done) { + expect(loadObj(objConcaveUrl, defaultOptions) + .then(function(data) { + var mesh = getMeshes(data)[0]; + var primitive = getPrimitives(data)[0]; + expect(mesh.positions.length / 3).toBe(30); + expect(primitive.indices.length).toBe(48); + }), done).toResolve(); + }); + it('loads obj with usemtl only', function(done) { expect(loadObj(objUsemtlUrl, defaultOptions) .then(function(data) {