Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and Tests #49

Merged
merged 29 commits into from
Apr 12, 2017
Merged

Refactor and Tests #49

merged 29 commits into from
Apr 12, 2017

Conversation

lilleyse
Copy link
Contributor

@lilleyse lilleyse commented Mar 14, 2017

This refactor fixes a few long-standing issues with this project:

  • Obj objects and groups were getting merged automatically based on material, which should be left as an optional optimization for gltf-pipeline. The gltf converted here is very true to the original obj.
  • Trouble with loading large models. This will now load obj files that are about 1 GB. Due to node memory limitations, it can't get much higher than that. In the future we can look into out-of-core loading, but the simplicity here is nice.
  • Some material improvements, especially for textured models without normals.
  • Added documentation and tests

To-do:

  • More testing - most models convert well when using --bypassPipeline, so there are some issues to look into for gltf-pipeline

This was referenced Mar 14, 2017
@lilleyse
Copy link
Contributor Author

Fixes #2 - power plant converts with the --bypassPipeline flag
Fixes #17 - no longer combines by material unless optimize is set.
Fixes #29 - speeds should improve since gltf-pipeline optimizations are disabled be default. Also --bypassPipeline is an available option for the fastest conversion.
Fixes #22 - same as above
Partial fix for #32 - using --bypassPipeline no techniques or shaders are generated. A material is still generated and this is required by the gltf 1.0 spec, but that will change in 2.0.
Fixes #34 - lighting should be improved for models without normals
Fixes #37 - however .mtl file had to change Tr 0 to Tr 1
Fixes #47 - normalization bugs have been fixed in CesiumGS/cesium#5032

@mramato
Copy link
Contributor

mramato commented Mar 14, 2017

Due to node memory limitations, it can't get much higher than that.

Can you elaborate on this? Because even without out-of-core processing, this seems like an architectural issue somewhere in the code.

@lilleyse
Copy link
Contributor Author

Yes maybe, I'm doing some more tests but the actual numbers do look better than that.

@lilleyse
Copy link
Contributor Author

lilleyse commented Mar 14, 2017

Maybe obj size isn't the best metric, but I ran through two highly tessellated spheres, one with smooth normals and one with face normals, both 3.2 GB. The first one converts while the second has enough indices that the index array exceeds the maximum allowable typed array length. This could be solved by splitting primitives, something I was trying to avoid - also maybe future work because I want to get these changes in pretty soon.

I actually don't think I have any "real" objs that fail at the moment, @shehzan10 had one that I still need to try out sometime tomorrow.

@mramato
Copy link
Contributor

mramato commented Mar 14, 2017

Thanks, that makes sense and sounds like a perfectly fine limitation until we need to handle massive obj files.

@pjcozzi
Copy link
Contributor

pjcozzi commented Mar 14, 2017

@likangning93 can you please review this?

@lilleyse
Copy link
Contributor Author

41956dd allows @shehzan10's model to be converted, but --separateTextures should be set. The problem before was there were enough images base64 encoded in the glTF that JSON.stringify fails (due to the same limitations that Node has on string sizes - can only be 192 MB or less)

@lilleyse
Copy link
Contributor Author

Ready now.

Copy link
Contributor

@likangning93 likangning93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at obj.js and spec to start off.

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 ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty inexperienced with regexes, so I could have used a note that the facePattern regexes return something like [generatedVertexID, position, texCoord, Normal]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wait, actually just saw the comments to the side... nevermind.

lib/obj.js Outdated
}

function useMaterial(name) {
// Look to see if this material has already been used by a primitive in the mesh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use a per-Mesh dictionary for materials?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I could have done a material -> primitive dictionary. Since nodes and meshes are also arrays I just wanted to keep it consistent.

lib/obj.js Outdated
for (var i = 0; i < primitivesLength; ++i) {
primitive = primitives[i]; // Sets the active primitive in case of returning early
if (primitive.material === material) {
return;
Copy link
Contributor

@likangning93 likangning93 Mar 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearer to do
primitive = primitives[i]; // Sets the active primitive in case of returning early
here, does that break anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is clearer.

lib/obj.js Outdated
.then(function(materials) {
var imagePaths = getImagePaths(materials);
return loadImages(imagePaths, objPath)
.then(function(images) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

less indentation:

return loadMaterials(...)
  .then(function(materials) {
     ///
   })
  .then(function(images) {
     ///
  });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside here is then materials is not in the closure of the final return (without a helper var).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah that's right... ok.

lib/obj.js Outdated
// Collect all the image files from the materials
var images = [];
function getImagePaths(materials) {
var imagePaths = [];
Copy link
Contributor

@likangning93 likangning93 Mar 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does indexOf do a linear search? Maybe a dictionary would be better here for an .obj with a huge number of textures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, will fix that.


describe('obj', function() {
it('loads obj with positions, normals, and uvs', function(done) {
expect(loadObj(objUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is there a difference between wrapping in expect(...).toResolve() and something like:

loadObj(..)
  .then(function(data)) {
    ///
    done();
  });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that way if the promise failed it wouldn't reach done(). The test would still fail due to a timeout, but the version in objSpec would fail faster.

}), done).toResolve();
});

it('loads obj with triangle faces', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate, was this supposed to be quads?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch. The other tests all use quads.

lib/image.js Outdated
.then(function(data) {
var extension = path.extname(imagePath);
var uriType = getUriType(extension);
var uri = uriType + ';base64,' + data.toString('base64');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this include a check on the buffer length for large textures? Otherwise, maybe we should add a troubleshooting section in the README.md with things to try, like writing out separate images if there's a buffer.toString error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah a common check like this could be in a few places.

lib/image.js Outdated
info.format = getFormat(channels);

if (channels === 4) {
// Need to do a finer grained check over the pixels to see if the image is actually transparent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, a lot of images may be 4-channel PNGs that don't have any transparent pixels. I wonder if it's okay to just make transparency a flag option, since a full pixel level check might be kind of slow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea for adding an option for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With one model I noticed a difference from 200 ms to 6000 ms just by checking this. (It had lots of textures though).

@lilleyse
Copy link
Contributor Author

Updated. The uri handling is better now.

}), done).toResolve();
});

it('bypassPipeline flag bypasses gltf-pipeline', function(done) {
var spy1 = spyOn(convert, '_outputJson');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to create these variables, just call spyOn.

expect(convert(objPath, gltfPath, options)
.then(function() {
expect(spy1.calls.count()).toBe(1);
expect(spy2.calls.count()).toBe(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better written as expect(GltfPipeline.processJSONToDisk).not.toHaveBeenCalled();

};
expect(convert(objPath, gltfPath, options)
.then(function() {
expect(spy1.calls.count()).toBe(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better written as expect(convert._outputJson).toHaveBeenCalled();

}), done).toResolve();
});

it('uses a custom logger', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test isn't really useful (and is also fragile if we change what we're logging). We should be testing for specific log messages in each instance we expect one to be generated, not just running something and seeing if the logger gets called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this.

I checked and the all the actually places things are logged are accounted for.

var fsExtra = require('fs-extra');
var path = require('path');
var Promise = require('bluebird');
var clone = require('../../lib/clone.js');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove .js from these requires.

var diffuseTexture;
var transparentDiffuseTexture;

beforeAll(function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to a beforeEach and get rid of all of the clone usage. You can then delete the clone.js.

lib/image.js Outdated

var fsReadFile = Promise.promisify(fs.readFile);
var defaultValue = Cesium.defaultValue;
var WebGLConstants = Cesium.WebGLConstants;

module.exports = loadImage;
Copy link
Contributor

@mramato mramato Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filename and function name don't match. Shouldn't the file be called loadImage?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a problem with most of the files in lib. They should all be fixed. We can do that in a quick separate PR after this is merged if you want, but please write up an issue.

lib/image.js Outdated
* @private
*/
function loadImage(imagePath, options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should check if imagePath is a string and throw a DeveloperError before doing anything else (also add a test for it).

return path.normalize(path.join(path.dirname(objPath), relativePath));
}

describe('mtl', function() {
Copy link
Contributor

@mramato mramato Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be a separate PR, but there are no tests here for how we handle invalid mtl files and we don't do any sort of parameter validation either. Please write up an issue to review all functions for error handling and parameter validation.

@mramato
Copy link
Contributor

mramato commented Apr 10, 2017

OK, that's all I have for the first go around. Given the size of this PR, there are definitely things we should do as separate PRs after this is merged, but let's make sure we write issues so nothing drops on the floor.

Thanks @lilleyse!

@lilleyse
Copy link
Contributor Author

Thanks for the thorough review @mramato, I'll get to these soon.

@lilleyse
Copy link
Contributor Author

lilleyse commented Apr 11, 2017

I got through a lot of the comments here. I may briefly scan everything again but I believe this is ready.

Some things I haven't addressed in this PR but will handle in others:

  • Better regexes with capture groups for loadMtl and loadObj
  • Simple validation (for obj, mtl, and images)
    • Use Check in all functions
  • Renaming files
  • Rethinking loadMtl and loadObj local functions approach.
  • Better logger messages Refactor and Tests #49 (comment)
  • Better error handling
  • Progress tracking
  • Add ability to find external files: Ability to find external files and textures  #54
  • Get around node string limitations. Build our own buffer ->utf-8 string stream?

Breaking change index.js will just export the convert function. Projects including obj2gltf do not need to call require('obj2gltf').convert(), just require('obj2gltf')().

@mramato
Copy link
Contributor

mramato commented Apr 12, 2017

Thanks @lilleyse! This is all good with me. I don't have commit access to this repository so someone else needs to his the merge button. (CC @pjcozzi)

Some of the future PR issues will make great beginner issues.

@pjcozzi
Copy link
Contributor

pjcozzi commented Apr 12, 2017

@lilleyse please make sure there are issues for everything in #49 (comment).

@pjcozzi pjcozzi merged commit 74a7349 into master Apr 12, 2017
@pjcozzi pjcozzi deleted the major-cleanup branch April 12, 2017 00:12
@pjcozzi
Copy link
Contributor

pjcozzi commented Apr 12, 2017

@lilleyse please comment close all the issues in #49 (comment).

@mramato
Copy link
Contributor

mramato commented Apr 12, 2017

@lilleyse when you get a chance, it would be great to publish a new version to npm as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants