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

Add support for Draco glTF models #6191

Merged
merged 22 commits into from
Mar 9, 2018
Merged

Add support for Draco glTF models #6191

merged 22 commits into from
Mar 9, 2018

Conversation

ggetz
Copy link
Contributor

@ggetz ggetz commented Feb 6, 2018

Part of #5120
Fixes #6298

Added support for Draco compressed models. Added sample models of the Mi;k Truck and Cesium Man.

TODO:

  • Include Quantization

@ggetz ggetz requested a review from lilleyse February 6, 2018 22:22
@cesium-concierge
Copy link

Signed CLA is on file.

@ggetz, thanks for the pull request! Maintainers, we have a signed CLA from @ggetz, so you can review this at any time.

⚠️ I noticed that a file in one of our ThirdParty folders (ThirdParty/, Source/ThirdParty/) has been added or modified. Please verify that it has a section in LICENSE.md and that its license information is up to date with this new version. Once you do, please confirm by commenting on this pull request.


I am a bot who helps you make Cesium awesome! Contributions to my configuration are welcome.

🌍 🌎 🌏

@ggetz ggetz changed the title Draco Add support for Draco glTF models Feb 6, 2018
@ggetz
Copy link
Contributor Author

ggetz commented Feb 6, 2018

Test is failing on Travis because of the following:

DeveloperError: IndexDatatype.UNSIGNED_INT requires OES_element_index_uint, which is not supported on this system.  Check context.elementIndexUint.

I use an an unsigned int for the index array, should I be using something else?

@pjcozzi
Copy link
Contributor

pjcozzi commented Feb 6, 2018

Exciting!

@ggetz please update this tasklist: #5120 (comment)

@lilleyse bump to me for a quick once over when you think this is ready.

Copy link
Contributor

@lilleyse lilleyse left a comment

Choose a reason for hiding this comment

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

The implementation looks really clean and self-contained - nice job!

Besides just loading the models to see if they worked I tested creating two of the same models to see how the new code interacted with the resource cache. Everything seems to be working fine.

@@ -158,6 +164,8 @@ define([
return {};
}

var dracoDecoder = new draco.Decoder();
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this should be initialized on demand in case this is a heavy object to add to the global state.

var typedArray = arraySlice(rawBuffer.extras._pipeline.source, bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength);

buffer = new draco.DecoderBuffer();
buffer.Init(new Int8Array(typedArray), bufferView.byteLength);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does Draco require an Int8Array instead of a Uint8Array? We should be careful because constructing a typed array from another typed array will copy the data, which we should try to avoid.

throw new RuntimeError('Unsupported draco mesh geometry type.');
}

dracoGeometry = new draco.Mesh();
Copy link
Contributor

Choose a reason for hiding this comment

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

This one is not getting destroyed later. It looks like the rest of the draco objects are good though.

@@ -4076,7 +4076,7 @@ define([
context : context,
typedArray : typedArray,
usage : BufferUsage.STATIC_DRAW,
indexDatatype : IndexDatatype.UNSIGNED_INT
indexDatatype : IndexDatatype.UNSIGNED_SHORT
Copy link
Contributor

Choose a reason for hiding this comment

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

We should test a model that has more than 65536 vertices. It will likely fail if UNSIGNED_SHORT is hardcoded. Could the index datatype be determined when reading then number of vertices in the compressed data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed this so it uses UNSIGNED_INT for more vertices. Do you know where I could find a model to test?

@ggetz
Copy link
Contributor Author

ggetz commented Feb 7, 2018

@lilleyse Updated. See #6191 (comment)

@lilleyse
Copy link
Contributor

lilleyse commented Feb 7, 2018

The new changes look good and the large model test worked.

@pjcozzi this is ready for you to look at.

@pjcozzi
Copy link
Contributor

pjcozzi commented Feb 7, 2018

  • How is code coverage? Unit tests look light.
  • Did we test with the sample models? Update Draco gltf models using ratified bitstream KhronosGroup/glTF-Sample-Models#145
  • How expensive is the decode for geometry-heavy models, e.g., a b3dm in a 3D tileset for a BIM model? Is it expensive enough that we should merge this into a staging branch and then merge everything into master once we are decoding on a web worker?
  • What is the size of the built release version of Cesium.js before and after this PR? The new draco-decoder-gltf.js is 712 KB. If the size increase is significant, what do we do? Move it to a web worker and be OK with the large download only when Draco is used? Provide a separate build that includes Draco support? I don't think the later is a good idea, but it could lead to other ideas. What do three.js and Babylon do?
    • There is also WIP for a pure JS decoder, but we'll likely want the webasm/emscripten one here for performance.

@ggetz
Copy link
Contributor Author

ggetz commented Feb 7, 2018

How is code coverage? Unit tests look light.

Coverage is good other than the error handling, I'll add test cases for those.

Did we test with the sample models? KhronosGroup/glTF-Sample-Models#145

Yes, tested a subsection, I can test them all if needed?

How expensive is the decode for geometry-heavy models, e.g., a b3dm in a 3D tileset for a BIM model? Is it expensive enough that we should merge this into a staging branch and then merge everything into master once we are decoding on a web worker?

I'll profile, but it shouldn't be too hard to move to a web worker since it's typed array in, typed array out.

If the size increase is significant, what do we do?

ThreeJS allows you to set the path of the module and load it at runtime, see here

@pjcozzi
Copy link
Contributor

pjcozzi commented Feb 7, 2018

Yes, tested a subsection, I can test them all if needed?

Absolutely.

They don't need to be submitted to the repo, just tested manually is OK unless they are needed for coverage.

@mramato
Copy link
Contributor

mramato commented Feb 8, 2018

Move it to a web worker and be OK with the large download only when Draco is used?

👍 for this approach. As @ggetz already mention, this should be a trivial change so it ca happen before this goes into master. Should also be faster too because we'll be decompressing multiple meshes in parallel.

EDIT: I was too curious not to check, final gzippped files size of Cesium.js went up 18% compared to master (722 KB to 865 KB). So that alone is reason enough to move it to a work and avoid loading it unless needed.

@pjcozzi
Copy link
Contributor

pjcozzi commented Feb 8, 2018

CC @cx20 - this is the PR to watch for Draco support in Cesium.

@ggetz
Copy link
Contributor Author

ggetz commented Mar 1, 2018

@lilleyse Updated!

The decoder module is now only included when a web worker is launched to decode a model. The decoding is concurrent, based on hardware limits, and parallel per primitive.

All the example models have been tested.

Is there a way to test the web workers directly? I'm testing the draco decoding one through modelSpec, but not directly.

});
});

fit('error decoding a glTF causes model loading to fail', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Accidentally left fit in here

var bufferView = gltf.bufferViews[compressionData.bufferView];
var rawBuffer = gltf.buffers[bufferView.buffer];
var data = rawBuffer.extras._pipeline.source;
data = data.slice(0, data.length);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use arraySlice instead.

var bufferView = gltf.bufferViews[compressionData.bufferView];
var rawBuffer = gltf.buffers[bufferView.buffer];
var data = rawBuffer.extras._pipeline.source;
data = data.slice(0, data.length);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to slice only the section of the buffer that Draco uses?

parseShaders(this);
parsePrograms(this);
parseTextures(this, context);
if (loadResources.decoding) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The interaction of gltf-pipeline updgrading, resource parsing, and draco loading is getting a bit confusing. They should be separated in a cleaner way. Also I realize some of the confusion was already here from before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Restructured, let me know if that's similar to what you had in mind.

return decodedAttributeData;
}

function decodeDracoPrimitive(parameters, transferableObjects) {
Copy link
Contributor

Choose a reason for hiding this comment

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

transferableObjects isn't used.

});
}

Model._maxDecodingConcurrency = Math.max(FeatureDetection.hardwareConcurrency - 1, 1); // Maximum concurrency to use wehn deocding draco models
Copy link
Contributor

Choose a reason for hiding this comment

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

Typos in comment.

promise = decoderTaskProcessor.scheduleTask(taskData, [taskData.array.buffer]);
}

while (defined(promise)) {
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 simplify the code to loop over loadResources.primitivesToDecode.length?

With that change the code below may only need to be called in one place.

            promise = undefined;
            taskData = loadResources.primitivesToDecode.peek();
            if (defined(taskData)) {
                promise = decoderTaskProcessor.scheduleTask(taskData, [taskData.array.buffer]);
            }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated this to be less redundant, but I'm hesitant to iterate through all of the primitives, as we don't necessarily iterate through all the primitives in the queue each frame. It depends on how many task can be scheduled as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, good point.


Model._maxDecodingConcurrency = Math.max(FeatureDetection.hardwareConcurrency - 1, 1); // Maximum concurrency to use wehn deocding draco models
Model._decoderTaskProcessor = undefined;
Model._getDecoderTaskProcessor = function () {
Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to add a comment that this is exposed for testing purposes.


if (attribute.data_type() === 4) {
attributeData = new draco.DracoInt32Array();
vertexArray = new Uint16Array(numPoints * numComponents);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have information about the max value in the DracoInt32Array? Ideally vertexArray should be either a Uint16Array or Uint32Array depending on the max value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This value isn't available through the draco API. We can only get it if we start iterating through and getting the values. Is it worth stopping and remaking the array if it should be Uint32?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably safe to just use uint16 then. I can't think of an integer attribute that commonly has elements above 65535. Leave a comment though explaining why a Uint16Array is used and the potential problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, will do. Int attributes were rarely used, I don't think the glTF example models use them at all.

dracoDecoder.GetAttributeFloatForAllPoints(dracoGeometry, attribute, attributeData);
}

for (var i = 0; i < vertexArray.length; ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cesium style note: factor out vertexArray.length to its own variable.

@@ -123,6 +123,9 @@ defineSuite([
var animatedMorphCubeUrl = './Data/Models/PBR/AnimatedMorphCube/AnimatedMorphCube.gltf';
var twoSidedPlaneUrl = './Data/Models/PBR/TwoSidedPlane/TwoSidedPlane.gltf';
var vertexColorTestUrl = './Data/Models/PBR/VertexColorTest/VertexColorTest.gltf';
var dracoCompressedModelUrl = './Data/Models/DracoCompression/CesiumMilkTruck/CesiumMilkTruck.gltf';
var dracoCompressedModelWithAnimationUrl = './Data/Models/DracoCompression/CesiumMan/CesiumMan.gltf';
var dracoCompressedModelWithErrorUrl = './Data/Models/DracoCompression/ShouldError/CesiumMilkTruck.gltf';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ShouldError/CesiumMilkTruck.gltf different than CesiumMilkTruck/CesiumMilkTruck.gltf? If not it can be removed.

Copy link
Contributor

@lilleyse lilleyse left a comment

Choose a reason for hiding this comment

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

For #6191 (comment) and #6191 (comment), the new restructuring looks great.

ForEach,
when
) {
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

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

Use slightly different formatting here:

        when) {
    'use strict';

};
}

function scheduleDecodingTask (decoderTaskProcessor, model, loadResources, context) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove space: function scheduleDecodingTask(

}

var bufferView = gltf.bufferViews[compressionData.bufferView];
//console.log(gltf.buffers[bufferView.buffer].extras._pipeline);
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove comment.


if (attribute.data_type() === 4) {
attributeData = new draco.DracoInt32Array();
vertexArray = new Uint16Array(numPoints * numComponents);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably safe to just use uint16 then. I can't think of an integer attribute that commonly has elements above 65535. Leave a comment though explaining why a Uint16Array is used and the potential problem.

@lilleyse
Copy link
Contributor

lilleyse commented Mar 8, 2018

Is [ ] Include Quantization something that is still planned for this PR?

@ggetz
Copy link
Contributor Author

ggetz commented Mar 8, 2018

Is [ ] Include Quantization something that is still planned for this PR?

I have another branch with the support that I will open in another PR - I need to confirm the test models and I don't want to slow down this one.

@ggetz
Copy link
Contributor Author

ggetz commented Mar 8, 2018

Thanks @lilleyse ! Addressed your remaining comments.

@lilleyse
Copy link
Contributor

lilleyse commented Mar 8, 2018

There are some sample models that aren't working. But they all have the same error so it should be easy to track down:

  • 2CylinderEngine
  • Buggy
  • GearboxAssy
  • ReciprocatingSaw

error

@ggetz
Copy link
Contributor Author

ggetz commented Mar 8, 2018

@lilleyse Fixed a race condition that was causing this error, and tested these models and a few others.

@lilleyse
Copy link
Contributor

lilleyse commented Mar 9, 2018

There is still something weird going on with those models:

buggy

buggy_error

It should look like:

buggy-actual

@ggetz
Copy link
Contributor Author

ggetz commented Mar 9, 2018

@lilleyse Sorry, missed that since the model had partially loaded. Updated and loading correctly now!

@mramato
Copy link
Contributor

mramato commented Mar 9, 2018

FYI, do to the build issue @ggetz found (and fixed) regarding including things that were only used on workers (and now that draco is only used on a worker), this PR actually shrinks the size of minified/gzipped Cesium.js by about ~40KB compared to mater! Awesome.

offset = (ix.byteOffset / IndexDatatype.getSizeInBytes(ix.componentType)); // glTF has offset in bytes. Cesium has offsets in indices
}
else {
var positions = accessors[primitive.attributes.POSITION];
count = positions.count;
offset = 0;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix formatting.

var bufferViewId = addBufferToModelResources(model, indexBuffer);
return {
bufferViewId: bufferViewId,
numberOfIndices : indexBuffer.numberOfIndices
Copy link
Contributor

Choose a reason for hiding this comment

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

Since numberOfIndices can get gotten from bufferViewId (though indirectly), it may be fine to not store numberOfIndices anywhere.

Up to you though, it doesn't matter much to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer getting all the needed data in one place, then just accessing it later. I did update comments to closely mirror the wording in the spec:

When loading each accessor, you must ignore the bufferView and go to the previously decoded Draco geometry in the primitive to get the data of indices and attributes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good.

@lilleyse
Copy link
Contributor

lilleyse commented Mar 9, 2018

Thanks @ggetz, can't wait to start using this with our data.

@lilleyse lilleyse merged commit 4ad27a4 into master Mar 9, 2018
@lilleyse lilleyse deleted the draco branch March 9, 2018 19:05
Copy link
Contributor

@pjcozzi pjcozzi left a comment

Choose a reason for hiding this comment

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

@ggetz looks pretty clean at quick glance. I would suggest updating the Sandcastle example with a "Load Draco compressed model" even if the code is exactly the same as the others with a comment pointing to https://github.com/google/draco or whatever content pipeline they need until ion does Draco compression.

The reference doc should also be updated to say that Model supports the Draco extension (and any other non-draft extensions that might have slipped through the cracks): https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Scene/Model.js#L1073

I won't have bandwidth for another review so please iterate with @lilleyse and ping me if you need something specific.

Also looking forward to the blog on this!

}

function addNewVertexBuffer(typedArray, model, context) {
var vertexBuffer = Buffer.createVertexBuffer({
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally Cesium only calls GL functions in the render loop. Is that the case here and below? If not, is the worth the rework to change?

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is the case.

@ggetz for this you could try integrating the draco loading with createBuffers which manages buffer creation from typed arrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made changes in #6341 that ensure the buffers are being created during Model.update, does that cover this? Or would it be better to integrate it into createBuffers?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be better, only for the sake of utilizing the JobScheduler. If it's easy to integrate I would try going for it.

However I did look at the approach in #6341 and it seems solid too.

for (var i = 0; i < numFaces; ++i) {
dracoDecoder.GetFaceFromMesh(dracoGeometry, i, faceIndices);

var offset = i * 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

Old school optimization that won't matter, but why not offset += 3; after the assignments to indexArray to avoid the multiplication?

'../Core/defined',
'../Core/IndexDatatype',
'../Core/RuntimeError',
'../ThirdParty/draco-decoder-gltf',
Copy link
Contributor

Choose a reason for hiding this comment

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

@fanzhanggoogle any chance you are able to quick a quick look at Cesium's use of the Draco API in this file and let us know if you have any feedback or tips? Thanks!

@@ -2330,6 +2336,49 @@ defineSuite([
});
});

it('loads a glTF with KHR_draco_mesh_compression extension', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these tests have code coverage for all the new code?

Do we test both with and without quantization?

Copy link
Contributor

@lilleyse lilleyse Mar 16, 2018

Choose a reason for hiding this comment

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

Do we test both with and without quantization?

I believe Draco + quantization was slated for the next PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I did not realize that quantization would be a separate PR. I was under the impression that Draco was "done." Let's please finish Draco with quantization before moving on to the GLSL extension.

This was referenced Mar 16, 2018
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.

5 participants