diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 74d9654..0e589dc 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -13,7 +13,6 @@ var defaultValue = util.defaultValue; // TODO : add command line flag for y-up to z-up // TODO : support zlib // TODO : support binary export -// TODO : generate normals if they don't exist if (process.argv.length < 3 || defined(argv.h) || defined(argv.help)) { console.log('Usage: ./bin/obj2gltf.js [INPUT] [OPTIONS]\n'); console.log(' -i, --input Path to obj file'); @@ -32,7 +31,7 @@ var combine = defaultValue(defaultValue(argv.c, argv.combine), false); var technique = defaultValue(argv.t, argv.technique); if (!defined(objFile)) { - console.error('-i or --input argument is required. See --help for details.'); + console.error('-i or --input argument is required. See --help for details.'); process.exit(1); } diff --git a/lib/obj.js b/lib/obj.js index c840455..591143e 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -4,11 +4,12 @@ var path = require('path'); var Material = require('./mtl'); var util = require('./util'); var defined = util.defined; -var defaultValue = util.defaultValue; +var normalize = util.normalize; +var faceNormal = util.faceNormal; module.exports = parseObj; -// Obj regex patterns are from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js) +// OBJ regex patterns are from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js) function getMaterials(contents, inputPath, done) { var hasMaterialGroups = /^usemtl/gm.test(contents); @@ -36,7 +37,9 @@ function parseObj(objFile, inputPath, done) { } getMaterials(contents, inputPath, function (materials) { - // A vertex in a face is specified by indexes into each of the attribute arrays, + var i, length; + + // A vertex is specified by indexes into each of the attribute arrays, // but these indexes may be different. This maps the separate indexes to a single index. var vertexCache = {}; var vertexCount = 0; @@ -55,7 +58,22 @@ function parseObj(objFile, inputPath, done) { var hasUVs = /^vt/gm.test(contents); if (!hasPositions) { - console.log('Could not process obj file, no positions.'); + console.log('Error: could not process OBJ file, no positions.'); + process.exit(1); + } + + // Auto-generate normals if they are missing from the obj file + var generateNormals = !hasNormals; + var vertexLocations = []; + var vertexNormals; + if (generateNormals) { + var locations = contents.match(/^v\s/gm).length; + vertexNormals = new Array(locations*3); + for (i = 0; i < locations; ++i) { + vertexNormals[i * 3 + 0] = 0; + vertexNormals[i * 3 + 1] = 0; + vertexNormals[i * 3 + 2] = 0; + } } // Map material to index array @@ -88,9 +106,6 @@ function parseObj(objFile, inputPath, done) { useDefaultMaterial(); } - // Sometimes the obj will have objects with different vertex formats - // e.g. one object has uvs, the other does not. - // In this case, use default values. function createVertex(p, u, n) { // Positions var pi = (parseInt(p) - 1) * 3; @@ -106,18 +121,28 @@ function parseObj(objFile, inputPath, done) { vertexArray.push(px, py, pz); // Normals - var ni = (parseInt(n) - 1) * 3; - var nx = defaultValue(normals[ni + 0], 0.0); - var ny = defaultValue(normals[ni + 1], 0.0); - var nz = defaultValue(normals[ni + 2], 0.0); - vertexArray.push(nx, ny, nz); + if (hasNormals) { + var ni = (parseInt(n) - 1) * 3; + var nx = normals[ni + 0]; + var ny = normals[ni + 1]; + var nz = normals[ni + 2]; + vertexArray.push(nx, ny, nz); + } else { + // Normals will be auto-generated later + vertexArray.push(0.0, 0.0, 0.0); + } // UVs if (hasUVs) { - var ui = (parseInt(u) - 1) * 2; - var ux = defaultValue(uvs[ui + 0], 0.0); - var uy = defaultValue(uvs[ui + 1], 0.0); - vertexArray.push(ux, uy); + if (defined(u)) { + var ui = (parseInt(u) - 1) * 2; + var ux = uvs[ui + 0]; + var uy = uvs[ui + 1]; + vertexArray.push(ux, uy); + } else { + // Some objects in the model may not have uvs, fill with 0's for consistency + vertexArray.push(0.0, 0.0); + } } } @@ -127,7 +152,13 @@ function parseObj(objFile, inputPath, done) { index = vertexCount++; vertexCache[v] = index; createVertex(p, u, n); + + if (generateNormals) { + var pi = (parseInt(p) - 1); + vertexLocations.push(pi); + } } + return index; } @@ -147,6 +178,36 @@ function parseObj(objFile, inputPath, done) { matIndexArray.push(index3); matIndexArray.push(index4); } + + if (generateNormals) { + // Get face normal + var i1 = (parseInt(p1) - 1) * 3; + var i2 = (parseInt(p2) - 1) * 3; + var i3 = (parseInt(p3) - 1) * 3; + var normal = faceNormal( + positions[i1], positions[i1 + 1], positions[i1 + 2], + positions[i2], positions[i2 + 1], positions[i2 + 2], + positions[i3], positions[i3 + 1], positions[i3 + 2] + ); + + // Add face normal to each vertex normal + vertexNormals[i1 + 0] += normal[0]; + vertexNormals[i1 + 1] += normal[1]; + vertexNormals[i1 + 2] += normal[2]; + vertexNormals[i2 + 0] += normal[0]; + vertexNormals[i2 + 1] += normal[1]; + vertexNormals[i2 + 2] += normal[2]; + vertexNormals[i3 + 0] += normal[0]; + vertexNormals[i3 + 1] += normal[1]; + vertexNormals[i3 + 2] += normal[2]; + + if (defined(v4)) { + var i4 = (parseInt(p4) - 1) * 3; + vertexNormals[i4 + 0] += normal[0]; + vertexNormals[i4 + 1] += normal[1]; + vertexNormals[i4 + 2] += normal[2]; + } + } } // v float float float @@ -171,8 +232,8 @@ function parseObj(objFile, inputPath, done) { var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/; var lines = contents.split('\n'); - var length = lines.length; - for (var i = 0; i < length; ++i) { + length = lines.length; + for (i = 0; i < length; ++i) { var line = lines[i].trim(); var result; if ((line.length === 0) || (line.charAt(0) === '#')) { @@ -184,18 +245,14 @@ function parseObj(objFile, inputPath, done) { parseFloat(result[3]) ); } else if ((result = normalPattern.exec(line) ) !== null) { - // Normalize var nx = parseFloat(result[1]); var ny = parseFloat(result[2]); var nz = parseFloat(result[3]); - var magnitude = Math.sqrt(nx * nx + ny * ny + nz * nz); - nx /= magnitude; - ny /= magnitude; - nz /= magnitude; + var normal = normalize(nx, ny, nz); normals.push( - nx, - ny, - nz + normal[0], + normal[1], + normal[2] ); } else if ((result = uvPattern.exec(line)) !== null) { uvs.push( @@ -205,17 +262,17 @@ function parseObj(objFile, inputPath, done) { } else if ((result = facePattern1.exec(line)) !== null) { addFace( - result[1], result[1], 0, 0, - result[2], result[2], 0, 0, - result[3], result[3], 0, 0, - result[4], result[4], 0, 0 + 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], 0, - result[4], result[5], result[6], 0, - result[7], result[8], result[9], 0, - result[10], result[11], result[12], 0 + 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( @@ -226,10 +283,10 @@ function parseObj(objFile, inputPath, done) { ); } else if ((result = facePattern4.exec(line)) !== null) { addFace( - result[1], result[2], 0, result[3], - result[4], result[5], 0, result[6], - result[7], result[8], 0, result[9], - result[10], result[11], 0, result[12] + 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 if (/^usemtl /.test(line)) { var materialName = line.substring(7).trim(); @@ -237,6 +294,25 @@ function parseObj(objFile, inputPath, done) { } } + if (generateNormals) { + length = vertexLocations.length; + for (i = 0; i < length; ++i) { + // Normalize normal + var index = vertexLocations[i] * 3; + var normal = normalize( + vertexNormals[index + 0], + vertexNormals[index + 1], + vertexNormals[index + 2] + ); + + // Set new normal in vertex array + var offset = i * (hasUVs ? 8 : 6) + 3; + vertexArray[offset + 0] = normal[0]; + vertexArray[offset + 1] = normal[1]; + vertexArray[offset + 2] = normal[2]; + } + } + done({ vertexCount: vertexCount, vertexArray: vertexArray, diff --git a/lib/util.js b/lib/util.js index 21b5cc4..e708e5c 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,7 +1,9 @@ "use strict"; module.exports = { defined : defined, - defaultValue : defaultValue + defaultValue : defaultValue, + normalize : normalize, + faceNormal : faceNormal }; function defined(value) { @@ -14,3 +16,26 @@ function defaultValue(a, b) { } return b; } + +var result = [0, 0, 0]; + +function normalize(x, y, z) { + var magnitude = Math.sqrt(x * x + y * y + z * z); + result[0] = x / magnitude; + result[1] = y / magnitude; + result[2] = z / magnitude; + return result; +} + +function faceNormal(x1, y1, z1, x2, y2, z2, x3, y3, z3) { + var e1x = x2 - x1; + var e1y = y2 - y1; + var e1z = z2 - z1; + var e2x = x3 - x1; + var e2y = y3 - y1; + var e2z = z3 - z1; + result[0] = (e1y * e2z) - (e1z * e2y); + result[1] = (e1z * e2x) - (e1x * e2z); + result[2] = (e1x * e2y) - (e1y * e2x); + return result; +}