diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 47acf97..40ffe76 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -122,6 +122,18 @@ const argv = yargs describe : 'The glTF will be saved with the KHR_materials_unlit extension.', type : 'boolean', default : defaults.unlit + }, + inputUpAxis : { + describe: 'Up axis of the obj.', + choices: ['X', 'Y', 'Z'], + type: 'string', + default: 'Y' + }, + outputUpAxis : { + describe: 'Up axis of the converted glTF.', + choices: ['X', 'Y', 'Z'], + type: 'string', + default: 'Y' } }).parse(args); @@ -167,7 +179,9 @@ const options = { specularGlossiness : argv.specularGlossiness, unlit : argv.unlit, overridingTextures : overridingTextures, - outputDirectory : outputDirectory + outputDirectory : outputDirectory, + inputUpAxis : argv.inputUpAxis, + outputUpAxis : argv.outputUpAxis }; console.time('Total'); diff --git a/lib/loadObj.js b/lib/loadObj.js index 90e4969..1052017 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -8,6 +8,7 @@ const loadMtl = require('./loadMtl'); const outsideDirectory = require('./outsideDirectory'); const readLines = require('./readLines'); +const Axis = Cesium.Axis; const Cartesian3 = Cesium.Cartesian3; const ComponentDatatype = Cesium.ComponentDatatype; const CoplanarPolygonGeometryLibrary = Cesium.CoplanarPolygonGeometryLibrary; @@ -16,6 +17,7 @@ const defined = Cesium.defined; const PolygonPipeline = Cesium.PolygonPipeline; const RuntimeError = Cesium.RuntimeError; const WindingOrder = Cesium.WindingOrder; +const Matrix4 = Cesium.Matrix4; module.exports = loadObj; @@ -47,6 +49,8 @@ const normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\ const uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vt float float const facePattern = /(-?\d+)\/?(-?\d*)\/?(-?\d*)/g; // for any face format "f v", "f v/v", "f v//v", "f v/v/v" +const scratchCartesian = new Cartesian3(); + /** * Parse an obj file. * @@ -57,6 +61,8 @@ const facePattern = /(-?\d+)\/?(-?\d*)\/?(-?\d*)/g; * @private */ function loadObj(objPath, options) { + const axisTransform = getAxisTransform(options.inputUpAxis, options.outputUpAxis); + // Global store of vertex attributes listed in the obj file let globalPositions = new ArrayStorage(ComponentDatatype.FLOAT); let globalNormals = new ArrayStorage(ComponentDatatype.FLOAT); @@ -354,9 +360,16 @@ function loadObj(objPath, options) { const mtllibLine = line.substring(7).trim(); mtlPaths = mtlPaths.concat(getMtlPaths(mtllibLine)); } else if ((result = vertexPattern.exec(line)) !== null) { - globalPositions.push(parseFloat(result[1])); - globalPositions.push(parseFloat(result[2])); - globalPositions.push(parseFloat(result[3])); + const position = scratchCartesian; + position.x = parseFloat(result[1]); + position.y = parseFloat(result[2]); + position.z = parseFloat(result[3]); + if (defined(axisTransform)) { + Matrix4.multiplyByPoint(axisTransform, position, position); + } + globalPositions.push(position.x); + globalPositions.push(position.y); + globalPositions.push(position.z); } else if ((result = normalPattern.exec(line) ) !== null) { const normal = Cartesian3.fromElements(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]), scratchNormal); if (Cartesian3.equals(normal, Cartesian3.ZERO)) { @@ -364,6 +377,9 @@ function loadObj(objPath, options) { } else { Cartesian3.normalize(normal, normal); } + if (defined(axisTransform)) { + Matrix4.multiplyByPointAsVector(axisTransform, normal, normal); + } globalNormals.push(normal.x); globalNormals.push(normal.y); globalNormals.push(normal.z); @@ -625,3 +641,19 @@ function cleanNodes(nodes) { setDefaults(nodes); return nodes; } + +function getAxisTransform(inputUpAxis, outputUpAxis) { + if (inputUpAxis === 'X' && outputUpAxis === 'Y') { + return Axis.X_UP_TO_Y_UP; + } else if (inputUpAxis === 'X' && outputUpAxis === 'Z') { + return Axis.X_UP_TO_Z_UP; + } else if (inputUpAxis === 'Y' && outputUpAxis === 'X') { + return Axis.Y_UP_TO_X_UP; + } else if (inputUpAxis === 'Y' && outputUpAxis === 'Z') { + return Axis.Y_UP_TO_Z_UP; + } else if (inputUpAxis === 'Z' && outputUpAxis === 'X') { + return Axis.Z_UP_TO_X_UP; + } else if (inputUpAxis === 'Z' && outputUpAxis === 'Y') { + return Axis.Z_UP_TO_Y_UP; + } +} diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 0f5a66c..cdb57e8 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -34,6 +34,8 @@ module.exports = obj2gltf; * @param {String} [options.overridingTextures.baseColorTexture] Path to the baseColor/diffuse texture. * @param {String} [options.overridingTextures.emissiveTexture] Path to the emissive texture. * @param {String} [options.overridingTextures.alphaTexture] Path to the alpha texture. + * @param {String} [options.inputUpAxis='Y'] Up axis of the obj. Choices are 'X', 'Y', and 'Z'. + * @param {String} [options.outputUpAxis='Y'] Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'. * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. * @param {Writer} [options.writer] A callback function that writes files that are saved as separate resources. * @param {String} [options.outputDirectory] Output directory for writing separate resources when options.writer is not defined. @@ -54,6 +56,8 @@ function obj2gltf(objPath, options) { options.overridingTextures = defaultValue(options.overridingTextures, defaultValue.EMPTY_OBJECT); options.logger = defaultValue(options.logger, getDefaultLogger()); options.writer = defaultValue(options.writer, getDefaultWriter(options.outputDirectory)); + options.inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); + options.outputUpAxis = defaultValue(options.outputUpAxis, defaults.outputUpAxis); if (!defined(objPath)) { throw new DeveloperError('objPath is required'); @@ -164,7 +168,19 @@ obj2gltf.defaults = { * @type Boolean * @default false */ - unlit : false + unlit : false, + /** + * Gets or sets the up axis of the obj. + * @type String + * @default 'Y' + */ + inputUpAxis: 'Y', + /** + * Gets or sets the up axis of the converted glTF. + * @type String + * @default 'Y' + */ + outputUpAxis: 'Y' }; /** diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index 7a84397..ca6f046 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -10,6 +10,7 @@ const clone = Cesium.clone; const RuntimeError = Cesium.RuntimeError; const objPath = 'specs/data/box/box.obj'; +const objRotatedUrl = 'specs/data/box-rotated/box-rotated.obj'; const objNormalsPath = 'specs/data/box-normals/box-normals.obj'; const objUvsPath = 'specs/data/box-uvs/box-uvs.obj'; const objPositionsOnlyPath = 'specs/data/box-positions-only/box-positions-only.obj'; @@ -455,6 +456,46 @@ describe('loadObj', () => { expect(primitives[3].indices.length).toBe(6); // 2 faces }); + function getFirstPosition(data) { + const primitive = getPrimitives(data)[0]; + return new Cartesian3(primitive.positions.get(0), primitive.positions.get(1), primitive.positions.get(2)); + } + + function getFirstNormal(data) { + const primitive = getPrimitives(data)[0]; + return new Cartesian3(primitive.normals.get(0), primitive.normals.get(1), primitive.normals.get(2)); + } + + async function checkAxisConversion(inputUpAxis, outputUpAxis, position, normal) { + const sameAxis = (inputUpAxis === outputUpAxis); + options.inputUpAxis = inputUpAxis; + options.outputUpAxis = outputUpAxis; + const data = await loadObj(objRotatedUrl, options); + const rotatedPosition = getFirstPosition(data); + const rotatedNormal = getFirstNormal(data); + if (sameAxis) { + expect(rotatedPosition).toEqual(position); + expect(rotatedNormal).toEqual(normal); + } else { + expect(rotatedPosition).not.toEqual(position); + expect(rotatedNormal).not.toEqual(normal); + } + } + + it('performs up axis conversion', async () => { + const data = await loadObj(objRotatedUrl, options); + const position = getFirstPosition(data); + const normal = getFirstNormal(data); + + const axes = ['X', 'Y', 'Z']; + const axesLength = axes.length; + for (let i = 0; i < axesLength; ++i) { + for (let j = 0; j < axesLength; ++j) { + await checkAxisConversion(axes[i], axes[j], position, normal); + } + } + }); + it('throws when file has invalid contents', async () => { let thrownError; try {