Merge branch 'master' into unlit

This commit is contained in:
Sean Lilley 2018-11-24 17:04:09 -05:00
commit b5da7be229
20 changed files with 724 additions and 422 deletions

View File

@ -1,3 +1,6 @@
{
"extends": "cesium/node"
"extends": "cesium/node",
"rules": {
"no-var": "off"
}
}

View File

@ -1,12 +1,25 @@
Change Log
==========
### 2.3.0 ???
### 2.3.2 2018-11-02
* 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
* Improved parsing models with concave or n-sided faces. [#157](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/157)
* Fixed handling of objs with interleaved materials. [#155](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/155)
### 2.3.0 2018-09-19
* Fixed handling of objs with mismatching attribute layouts. [#153](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/153)
* Fixed normalization of 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

@ -1,5 +1,7 @@
# OBJ2GLTF
[![Greenkeeper badge](https://badges.greenkeeper.io/AnalyticalGraphicsInc/obj2gltf.svg)](https://greenkeeper.io/)
Convert OBJ assets to [glTF](https://www.khronos.org/gltf) 2.0.
## Getting Started

View File

@ -22,7 +22,14 @@ process.env.PATH += environmentSeparator + nodeBinaries;
var specFiles = ['**/*.js', '!node_modules/**', '!coverage/**', '!doc/**', '!bin/**'];
gulp.task('test', function (done) {
module.exports = {
test: test,
'test-watch': testWatch,
coverage: coverage,
cloc: cloc
};
function test(done) {
var jasmine = new Jasmine();
jasmine.loadConfigFile('specs/jasmine.json');
jasmine.addReporter(new JasmineSpecReporter({
@ -32,10 +39,10 @@ gulp.task('test', function (done) {
jasmine.onComplete(function (passed) {
done(argv.failTaskOnError && !passed ? 1 : 0);
});
});
}
gulp.task('test-watch', function () {
gulp.watch(specFiles).on('change', function () {
function testWatch() {
return gulp.watch(specFiles).on('change', function () {
// We can't simply depend on the test task because Jasmine
// does not like being run multiple times in the same process.
try {
@ -46,9 +53,9 @@ gulp.task('test-watch', function () {
console.log('Tests failed to execute.');
}
});
});
}
gulp.task('coverage', function () {
async function coverage() {
fsExtra.removeSync('coverage/server');
child_process.execSync('nyc' +
' --all' +
@ -57,13 +64,12 @@ gulp.task('coverage', function () {
' -x "specs/**" -x "coverage/**" -x "doc/**" -x "bin/**" -x "index.js" -x "gulpfile.js"' +
' node_modules/jasmine/bin/jasmine.js' +
' JASMINE_CONFIG_PATH=specs/jasmine.json', {
stdio: [process.stdin, process.stdout, process.stderr]
});
stdio: [process.stdin, process.stdout, process.stderr]
});
open('coverage/lcov-report/index.html');
});
}
gulp.task('cloc', function() {
function cloc() {
var cmdLine;
var clocPath = path.join('node_modules', 'cloc', 'lib', 'cloc');
@ -99,4 +105,4 @@ gulp.task('cloc', function() {
});
});
});
});
}

View File

@ -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 : {},
@ -133,10 +137,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);
@ -198,6 +200,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)) {
@ -211,7 +238,7 @@ function resolveTextures(gltf, material) {
}
}
function addMaterial(gltf, material, options) {
function addGltfMaterial(gltf, material, options) {
resolveTextures(gltf, material);
var materialIndex = gltf.materials.length;
if (options.unlit) {
@ -224,43 +251,94 @@ function addMaterial(gltf, material, options) {
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, options) {
var material = getMaterialByName(materials, materialName);
var materialIndex = getMaterialIndex(gltf.materials, materialName);
if (!defined(materialIndex)) {
materialIndex = addMaterial(gltf, material, options);
materialIndex = addGltfMaterial(gltf, material, options);
}
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);
@ -305,68 +383,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 = getOrCreateGltfMaterial(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

@ -81,19 +81,15 @@ function loadMtl(mtlPath, options) {
materials.push(material);
}
/**
* Removes texture options from texture name
* NOTE: assumes no spaces in texture name
*
* @param {String} name
* @returns {String} The clean texture name
*/
function cleanTextureName (name) {
function normalizeTexturePath(texturePath, mtlDirectory) {
// Removes texture options from texture name
// Assumes no spaces in texture name
var re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/;
if (re.test(name)) {
return name.split(/\s+/).pop();
if (re.test(texturePath)) {
texturePath = texturePath.split(/\s+/).pop();
}
return name;
texturePath = texturePath.replace(/\\/g, '/');
return path.normalize(path.join(mtlDirectory, texturePath));
}
function parseLine(line) {
@ -144,31 +140,31 @@ function loadMtl(mtlPath, options) {
material.alpha = correctAlpha(1.0 - parseFloat(value));
} else if (/^map_Ka /i.test(line)) {
if (!defined(overridingAmbientTexture)) {
material.ambientTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim()));
material.ambientTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
}
} else if (/^map_Ke /i.test(line)) {
if (!defined(overridingEmissiveTexture)) {
material.emissiveTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim()));
material.emissiveTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
}
} else if (/^map_Kd /i.test(line)) {
if (!defined(overridingDiffuseTexture)) {
material.diffuseTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim()));
material.diffuseTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
}
} else if (/^map_Ks /i.test(line)) {
if (!defined(overridingSpecularTexture)) {
material.specularTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim()));
material.specularTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
}
} else if (/^map_Ns /i.test(line)) {
if (!defined(overridingSpecularShininessTexture)) {
material.specularShininessTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim()));
material.specularShininessTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
}
} else if (/^map_Bump /i.test(line)) {
if (!defined(overridingNormalTexture)) {
material.normalTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(9).trim()));
material.normalTexture = normalizeTexturePath(line.substring(9).trim(), mtlDirectory);
}
} else if (/^map_d /i.test(line)) {
if (!defined(overridingAlphaTexture)) {
material.alphaTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(6).trim()));
material.alphaTexture = normalizeTexturePath(line.substring(6).trim(), mtlDirectory);
}
}
}
@ -244,7 +240,7 @@ function loadMaterialTexture(material, name, textureOptions, mtlDirectory, textu
var texturePromise = texturePromiseMap[texturePath];
if (!defined(texturePromise)) {
var shallowPath = path.resolve(path.join(mtlDirectory, path.basename(texturePath)));
var shallowPath = path.join(mtlDirectory, path.basename(texturePath));
if (options.secure && outsideDirectory(texturePath, mtlDirectory)) {
// Try looking for the texture in the same directory as the obj
options.logger('Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead.');

View File

@ -8,17 +8,12 @@ var loadMtl = require('./loadMtl');
var outsideDirectory = require('./outsideDirectory');
var readLines = require('./readLines');
var Cartesian2 = Cesium.Cartesian2;
var Cartesian3 = Cesium.Cartesian3;
var ComponentDatatype = Cesium.ComponentDatatype;
var CoplanarPolygonGeometryLibrary = Cesium.CoplanarPolygonGeometryLibrary;
var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined;
var IntersectionTests = Cesium.IntersectionTests;
var Matrix3 = Cesium.Matrix3;
var OrientedBoundingBox = Cesium.OrientedBoundingBox;
var Plane = Cesium.Plane;
var PolygonPipeline = Cesium.PolygonPipeline;
var Ray = Cesium.Ray;
var RuntimeError = Cesium.RuntimeError;
var WindingOrder = Cesium.WindingOrder;
@ -36,14 +31,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 +71,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;
@ -94,7 +89,10 @@ function loadObj(objPath, options) {
var faceUvs = [];
var faceNormals = [];
var vertexIndices = [];
function clearVertexCache() {
vertexCache = {};
vertexCacheCount = 0;
}
function getName(name) {
return (name === '' ? undefined : name);
@ -112,36 +110,56 @@ 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
clearVertexCache();
vertexCount = 0;
}
function useMaterial(name) {
var material = getName(name);
activeMaterial = material;
// Look to see if this material has already been used by a primitive in the mesh
function reusePrimitive(callback) {
var primitives = mesh.primitives;
var primitivesLength = primitives.length;
for (var i = 0; i < primitivesLength; ++i) {
if (primitives[i].material === material) {
primitive = primitives[i];
return;
if (primitives[i].material === activeMaterial) {
if (!defined(callback) || callback(primitives[i])) {
primitive = primitives[i];
clearVertexCache();
vertexCount = primitive.positions.length / 3;
return;
}
}
}
// Add a new primitive with this material
addPrimitive();
}
function useMaterial(name) {
activeMaterial = getName(name);
reusePrimitive();
}
function faceAndPrimitiveMatch(uvs, normals, primitive) {
var faceHasUvs = uvs[0].length > 0;
var faceHasNormals = normals[0].length > 0;
var primitiveHasUvs = primitive.uvs.length > 0;
var primitiveHasNormals = primitive.normals.length > 0;
return primitiveHasUvs === faceHasUvs && primitiveHasNormals === faceHasNormals;
}
function checkPrimitive(uvs, normals) {
var firstFace = primitive.indices.length === 0;
if (!firstFace && !faceAndPrimitiveMatch(uvs, normals, primitive)) {
reusePrimitive(function(primitive) {
return faceAndPrimitiveMatch(uvs, normals, primitive);
});
}
}
function getOffset(a, attributeData, components) {
var i = parseInt(a);
if (i < 0) {
@ -158,9 +176,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 +187,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 +197,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);
}
}
@ -195,114 +213,13 @@ function loadObj(objPath, options) {
// may be some duplicate vertices.
vertexCacheCount++;
if (vertexCacheCount > vertexCacheLimit) {
vertexCacheCount = 0;
vertexCache = {};
clearVertexCache();
}
}
return index;
}
// 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);
}
Plane.fromPointNormal(scratchOrigin, scratchNormal, scratchPlane);
scratchRay.direction = scratchNormal;
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) {
function getPosition(index, result) {
var pi = getOffset(index, positions, 3);
var px = positions.get(pi + 0);
var py = positions.get(pi + 1);
@ -310,7 +227,7 @@ function loadObj(objPath, options) {
return Cartesian3.fromElements(px, py, pz, result);
}
function get3DNormal(index, result) {
function getNormal(index, result) {
var ni = getOffset(index, normals, 3);
var nx = normals.get(ni + 0);
var ny = normals.get(ni + 1);
@ -318,36 +235,28 @@ function loadObj(objPath, options) {
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 scratchCenter = new Cartesian3();
var scratchAxis1 = new Cartesian3();
var scratchAxis2 = new Cartesian3();
var scratchNormal = new Cartesian3();
var scratchPositions = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];
var scratchVertexIndices = [];
var scratchPoints = [];
function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normalIndex) {
if (normalIndex.length === 0) {
// If no face normal, we have to assume the winding is correct.
return true;
}
var normal = getNormal(normalIndex, scratchNormal);
var A = getPosition(positionIndex1, scratch1);
var B = getPosition(positionIndex2, scratch2);
var C = getPosition(positionIndex3, scratch3);
var BA = Cartesian3.subtract(B, A, scratch4);
var CA = Cartesian3.subtract(C, A, scratch5);
@ -368,58 +277,45 @@ function loadObj(objPath, options) {
}
}
var scratchPositions3D = [new Cartesian3(), new Cartesian3(), new Cartesian3()];
function addFace(vertices, positions, uvs, normals) {
var isWindingCorrect = true;
var faceNormal;
var i;
var isWindingCorrect;
// 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);
}
checkPrimitive(uvs, normals);
if (vertices.length === 3) {
isWindingCorrect = checkWindingCorrect(positions[0], positions[1], positions[2], normals[0]);
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 = [];
var points = scratchPoints;
var vertexIndices = scratchVertexIndices;
points.length = 0;
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());
if (i === scratchPositions.length) {
scratchPositions.push(new Cartesian3());
}
positions3D.push(get3DPoint(positions[i], scratchPositions3D[i]));
points.push(getPosition(positions[i], scratchPositions[i]));
}
var positions2D = projectTo2D(positions3D);
var validGeometry = CoplanarPolygonGeometryLibrary.computeProjectTo2DArguments(points, scratchCenter, scratchAxis1, scratchAxis2);
if (!validGeometry) {
return;
}
var projectPoints = CoplanarPolygonGeometryLibrary.createProjectPointsTo2DFunction(scratchCenter, scratchAxis1, scratchAxis2);
var points2D = projectPoints(points);
var indices = PolygonPipeline.triangulate(points2D);
isWindingCorrect = PolygonPipeline.computeWindingOrder2D(points2D) !== WindingOrder.CLOCKWISE;
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);
}
for (i = 0; i < indices.length - 2; i += 3) {
addTriangle(vertexIndices[indices[i]], vertexIndices[indices[i+1]], vertexIndices[indices[i+2]], isWindingCorrect);
}
}
}
@ -502,7 +398,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 +419,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 +427,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,
@ -540,6 +438,11 @@ function finishLoading(nodes, mtlPaths, objPath, options) {
});
}
function normalizeMtlPath(mtlPath, objDirectory) {
mtlPath = mtlPath.replace(/\\/g, '/');
return path.normalize(path.join(objDirectory, mtlPath));
}
function loadMtls(mtlPaths, objPath, options) {
var objDirectory = path.dirname(objPath);
var materials = [];
@ -550,8 +453,8 @@ function loadMtls(mtlPaths, objPath, options) {
});
return Promise.map(mtlPaths, function(mtlPath) {
mtlPath = path.resolve(objDirectory, mtlPath);
var shallowPath = path.resolve(path.join(objDirectory, path.basename(mtlPath)));
mtlPath = normalizeMtlPath(mtlPath, objDirectory);
var shallowPath = path.join(objDirectory, path.basename(mtlPath));
if (options.secure && outsideDirectory(mtlPath, objDirectory)) {
// Try looking for the .mtl in the same directory as the obj
options.logger('The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.');
@ -586,9 +489,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) {
@ -609,10 +509,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

@ -1,6 +1,6 @@
{
"name": "obj2gltf",
"version": "2.2.0",
"version": "2.3.2",
"description": "Convert OBJ model format to glTF",
"license": "Apache-2.0",
"contributors": [
@ -26,26 +26,26 @@
"node": ">=4.0.0"
},
"dependencies": {
"bluebird": "^3.5.1",
"cesium": "^1.39.0",
"fs-extra": "^4.0.2",
"jpeg-js": "^0.3.3",
"mime": "^2.0.3",
"pngjs": "^3.3.0",
"yargs": "^10.0.3"
"bluebird": "^3.5.2",
"cesium": "^1.50.0",
"fs-extra": "^7.0.0",
"jpeg-js": "^0.3.4",
"mime": "^2.3.1",
"pngjs": "^3.3.3",
"yargs": "^12.0.2"
},
"devDependencies": {
"cloc": "^2.3.3",
"coveralls": "^3.0.0",
"eslint": "^4.4.1",
"eslint-config-cesium": "^2.0.1",
"gulp": "^3.9.1",
"jasmine": "^2.7.0",
"jasmine-spec-reporter": "^4.2.0",
"jsdoc": "^3.5.4",
"nyc": "^11.1.0",
"cloc": "^2.3.4",
"coveralls": "^3.0.2",
"eslint": "^5.6.1",
"eslint-config-cesium": "^6.0.0",
"gulp": "^4.0.0",
"jasmine": "^3.2.0",
"jasmine-spec-reporter": "^4.2.1",
"jsdoc": "^3.5.5",
"nyc": "^13.0.1",
"open": "^0.0.5",
"requirejs": "^2.3.4"
"requirejs": "^2.3.6"
},
"scripts": {
"jsdoc": "jsdoc ./lib -R ./README.md -d doc",

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

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

@ -1,19 +1,19 @@
# Blender MTL File: 'box.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 0.200000 0.200000 0.200000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.100000 0.100000 0.100000
Ni 1.000000
d 0.900000
Tr 0.100000
map_Ka -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 ambient.gif
map_Ke -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 emission.jpg
map_Kd -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 diffuse.png
map_Ks -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 specular.jpeg
map_Ns -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 shininess.png
map_Bump -bm 0.2 bump.png
illum 2
# Blender MTL File: 'box.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 0.200000 0.200000 0.200000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.100000 0.100000 0.100000
Ni 1.000000
d 0.900000
Tr 0.100000
map_Ka -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 ambient.gif
map_Ke -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 emission.jpg
map_Kd -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 diffuse.png
map_Ks -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 specular.jpeg
map_Ns -s 1.0 1.0 1.0 -o 0.0 0.0 0.0 shininess.png
map_Bump -bm 0.2 bump.png
illum 2

View File

@ -1,46 +1,46 @@
# Blender v2.78 (sub 0) OBJ File: 'box.blend'
# www.blender.org
mtllib box-texture-options.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/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
# Blender v2.78 (sub 0) OBJ File: 'box.blend'
# www.blender.org
mtllib box-texture-options.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/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

View File

@ -0,0 +1,46 @@
# Blender v2.78 (sub 0) OBJ File: 'box.blend'
# www.blender.org
mtllib materials\\box-windows-paths.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/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

View File

@ -0,0 +1,13 @@
# Blender MTL File: 'box.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
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
illum 2
map_Kd .\images\cesium.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

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

@ -34,9 +34,11 @@ var objExternalResourcesInRootPath = 'specs/data/box-external-resources-in-root/
var objTexturedPath = 'specs/data/box-textured/box-textured.obj';
var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.obj';
var objSubdirectoriesPath = 'specs/data/box-subdirectories/box-textured.obj';
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) {
@ -93,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();
@ -104,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();
});
@ -115,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);
@ -131,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();
});
@ -144,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();
});
@ -153,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();
});
@ -255,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();
});
@ -309,6 +309,13 @@ describe('loadObj', function() {
expect(primitives[0].material).toBe('Red');
expect(primitives[1].material).toBe('Green');
expect(primitives[2].material).toBe('Blue');
for (var i = 0; i < 3; ++i) {
var indices = primitives[i].indices;
for (var j = 0; j < indices.length; ++j) {
expect(indices.get(j)).toBeLessThan(8);
}
}
}), done).toResolve();
});
@ -478,6 +485,27 @@ describe('loadObj', function() {
}), done).toResolve();
});
it('loads obj with windows paths', function(done) {
expect(loadObj(objWindowsPaths, options)
.then(function(data) {
var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe('cesium');
expect(baseColorTexture.source).toBeDefined();
}), done).toResolve();
});
it('separates faces that don\'t use the same attributes as other faces in the primitive', function(done) {
expect(loadObj(objMixedAttributesPath, options)
.then(function(data) {
var primitives = getPrimitives(data);
expect(primitives.length).toBe(4);
expect(primitives[0].indices.length).toBe(18); // 6 faces
expect(primitives[1].indices.length).toBe(6); // 2 faces
expect(primitives[2].indices.length).toBe(6); // 2 faces
expect(primitives[3].indices.length).toBe(6); // 2 faces
}), done).toResolve();
});
it('throws when file has invalid contents', function(done) {
expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError);
});