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
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Change Log
==========

### 1.44 - 2018-04-02

##### Additions :tada:
* Added support for glTF models with [Draco geometry compression](https://github.com/fanzhanggoogle/glTF/blob/KHR_mesh_compression/extensions/Khronos/KHR_draco_mesh_compression/README.md).

### 1.43 - 2018-03-01

##### Major Announcements :loudspeaker:
Expand Down
16 changes: 16 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,22 @@ https://github.com/KhronosGroup/glTF-WebGL-PBR
>CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
>OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

### Draco

https://github.com/google/draco

>Licensed under the Apache License, Version 2.0 (the "License"); you may not
>use this file except in compliance with the License. You may obtain a copy of
>the License at
>
><http://www.apache.org/licenses/LICENSE-2.0>
>
>Unless required by applicable law or agreed to in writing, software
>distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
>WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
>License for the specific language governing permissions and limitations under
>the License.

Tests
=====

Expand Down
247 changes: 220 additions & 27 deletions Source/Scene/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ define([
'../Core/clone',
'../Core/Color',
'../Core/combine',
'../Core/ComponentDatatype',
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
Expand All @@ -34,6 +35,7 @@ define([
'../Core/Quaternion',
'../Core/Queue',
'../Core/RuntimeError',
'../Core/TaskProcessor',
'../Core/Transforms',
'../Core/WebGLConstants',
'../Renderer/Buffer',
Expand Down Expand Up @@ -84,6 +86,7 @@ define([
clone,
Color,
combine,
ComponentDatatype,
defaultValue,
defined,
defineProperties,
Expand All @@ -110,6 +113,7 @@ define([
Quaternion,
Queue,
RuntimeError,
TaskProcessor,
Transforms,
WebGLConstants,
Buffer,
Expand Down Expand Up @@ -616,6 +620,7 @@ define([
this._cachedRendererResources = undefined;
this._loadRendererResourcesFromCache = false;
this._updatedGltfVersion = false;
this._decodedData = {};

this._cachedGeometryByteLength = 0;
this._cachedTexturesByteLength = 0;
Expand Down Expand Up @@ -1369,8 +1374,11 @@ define([
// through glTF accessors to create the bufferview's index buffer.
ForEach.accessor(model.gltf, function(accessor) {
var bufferViewId = accessor.bufferView;
var bufferView = bufferViews[bufferViewId];
if (!defined(bufferViewId)) {
return;
}

var bufferView = bufferViews[bufferViewId];
if ((bufferView.target === WebGLConstants.ELEMENT_ARRAY_BUFFER) && !defined(indexBufferIds[bufferViewId])) {
indexBufferIds[bufferViewId] = true;
indexBuffersToCreate.enqueue({
Expand Down Expand Up @@ -2387,6 +2395,7 @@ define([

for (var i = 0; i < primitivesLength; ++i) {
var primitive = primitives[i];
var decodedData = model._decodedData[meshId + '.primitive.' + i];

// GLTF_SPEC: This does not take into account attribute arrays,
// indicated by when an attribute points to a parameter with a
Expand All @@ -2407,6 +2416,26 @@ define([
// with an attribute that wasn't used and the asset wasn't optimized.
if (defined(attributeLocation)) {
var a = accessors[primitiveAttributes[attributeName]];

// Use decoded draco attributes if available
if (defined(decodedData)) {
var decodedAttributes = decodedData.attributes;
if (decodedAttributes.hasOwnProperty(attributeName)) {
var decodedAttribute = decodedAttributes[attributeName];
attributes.push({
index : attributeLocation,
vertexBuffer : rendererBuffers[decodedAttribute.bufferView],
componentsPerAttribute : decodedAttribute.componentsPerAttribute,
componentDatatype : decodedAttribute.componentDatatype,
normalize: decodedAttribute.normalized,
offsetInBytes : decodedAttribute.byteOffset,
strideInBytes : decodedAttribute.byteStride
});

continue;
}
}

var normalize = false;
if (defined(a.normalized) && a.normalized) {
normalize = true;
Expand Down Expand Up @@ -2443,7 +2472,14 @@ define([
var indexBuffer;
if (defined(primitive.indices)) {
var accessor = accessors[primitive.indices];
indexBuffer = rendererBuffers[accessor.bufferView];
var bufferView = accessor.bufferView;

// Used decoded draco buffer if available
if (defined(decodedData)) {
bufferView = decodedData.bufferView;
}

indexBuffer = rendererBuffers[bufferView];
}
rendererVertexArrays[meshId + '.primitive.' + i] = new VertexArray({
context : context,
Expand Down Expand Up @@ -4009,6 +4045,151 @@ define([
return (distance2 >= nearSquared) && (distance2 <= farSquared);
}

///////////////////////////////////////////////////////////////////////////

function addBufferToModelResources(model, buffer) {
var resourceBuffers = model._rendererResources.buffers;
var id = Object.keys(resourceBuffers).length;
resourceBuffers[id] = buffer;
model._geometryByteLength += buffer.sizeInBytes;

return id;
}

function addNewVertexBuffer(typedArray, model, context) {
var vertexBuffer = Buffer.createVertexBuffer({
context : context,
typedArray : typedArray,
usage : BufferUsage.STATIC_DRAW
});
vertexBuffer.vertexArrayDestroyable = false;

return addBufferToModelResources(model, vertexBuffer);
}

function addNewIndexBuffer(typedArray, model, context) {
var indexBuffer = Buffer.createIndexBuffer({
context : context,
typedArray : typedArray,
usage : BufferUsage.STATIC_DRAW,
indexDatatype : ComponentDatatype.fromTypedArray(typedArray)
});
indexBuffer.vertexArrayDestroyable = false;

return addBufferToModelResources(model, indexBuffer);
}

function addDecodededBuffers(primitive, model, context) {
return function (result) {
var decodedBufferView = addNewIndexBuffer(result.indexArray, model, context);

var attributes = {};
var decodedAttributeData = result.attributeData;
for (var attributeName in decodedAttributeData) {
if (decodedAttributeData.hasOwnProperty(attributeName)) {
var attribute = decodedAttributeData[attributeName];
var vertexArray = attribute.array;
var vertexBufferView = addNewVertexBuffer(vertexArray, model, context);

var data = attribute.data;
data.bufferView = vertexBufferView;

attributes[attributeName] = data;
}
}

model._decodedData[primitive.mesh + '.primitive.' + primitive.primitive] = {
bufferView : decodedBufferView,
attributes : attributes
};
};
}

function parseDraco(model, context) {
if (!defined(model.extensionsRequired['KHR_draco_mesh_compression'])
|| !defined(model.extensionsUsed['KHR_draco_mesh_compression'])) {
return;
}

var loadResources = model._loadResources;
if (loadResources.primitivesToDecode.length === 0) {
if (loadResources.decoding) {
// Done decoding
return;
}

loadResources.decoding = true;

var gltf = model.gltf;
ForEach.mesh(gltf, function(mesh, meshId) {
ForEach.meshPrimitive(mesh, function(primitive, primitiveId) {
if (!defined(primitive.extensions)) {
return;
}

var compressionData = primitive.extensions['KHR_draco_mesh_compression'];
if (!defined(compressionData)) {
return;
}

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.

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?


loadResources.primitivesToDecode.enqueue({
mesh : meshId,
primitive : primitiveId,
array : data,
bufferView : bufferView,
compressedAttributes : compressionData.attributes
});
});
});
}

var decoderTaskProcessor = Model._getDecoderTaskProcessor();
var taskData = loadResources.primitivesToDecode.peek();
var decodingPromises = [];
var promise;

if (defined(taskData)) {
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.

loadResources.finishedDecoding = false;
loadResources.primitivesToDecode.dequeue();
var decodedPromise = promise.then(addDecodededBuffers(taskData, model, context))
.otherwise(function (error) {
model._state = ModelState.FAILED;
model._readyPromise.reject(new RuntimeError('Failed to load model: ' + model.basePath + '\n' + error.message));
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Use getFailedLoadFunction to avoid duplicate error handling code.


decodingPromises.push(decodedPromise);

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

when.all(decodingPromises).then(function () {
loadResources.finishedDecoding = true;
});
}

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.

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 (!defined(Model._decoderTaskProcessor)) {
Model._decoderTaskProcessor = new TaskProcessor('decodeDraco', Model._maxDecodingConcurrency);
}

return Model._decoderTaskProcessor;
};

/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
* get the draw commands needed to render this primitive.
Expand Down Expand Up @@ -4094,38 +4275,50 @@ define([
// Textures may continue to stream in while in the LOADED state.
if (loadResources.pendingBufferLoads === 0) {
if (!this._updatedGltfVersion) {
var options = {
optimizeForCesium: true,
addBatchIdToGeneratedShaders : this._addBatchIdToGeneratedShaders
};
frameState.brdfLutGenerator.update(frameState);
updateVersion(this.gltf);
ModelUtility.checkSupportedExtensions(this.extensionsRequired);
addPipelineExtras(this.gltf);
addDefaults(this.gltf);
processModelMaterialsCommon(this.gltf, options);
processPbrMetallicRoughness(this.gltf, options);
// We do this after to make sure that the ids don't change
addBuffersToLoadResources(this);

if (!this._loadRendererResourcesFromCache) {
parseBufferViews(this);
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.

parseDraco(this, context);
} else {
var options = {
optimizeForCesium: true,
addBatchIdToGeneratedShaders : this._addBatchIdToGeneratedShaders
};
frameState.brdfLutGenerator.update(frameState);
updateVersion(this.gltf);
ModelUtility.checkSupportedExtensions(this.extensionsRequired);
addPipelineExtras(this.gltf);
addDefaults(this.gltf);
processModelMaterialsCommon(this.gltf, options);
processPbrMetallicRoughness(this.gltf, options);

parseDraco(this, context);
}
parseMaterials(this);
parseMeshes(this);
parseNodes(this);

this._boundingSphere = computeBoundingSphere(this);
this._initialRadius = this._boundingSphere.radius;
this._updatedGltfVersion = true;
// We must wait until the geometry is decoded
if (loadResources.decodingComplete()) {
// We do this after to make sure that the ids don't change
addBuffersToLoadResources(this);

if (!this._loadRendererResourcesFromCache) {
parseBufferViews(this);
parseShaders(this);
parsePrograms(this);
parseTextures(this, context);
}
parseMaterials(this);
parseMeshes(this);
parseNodes(this);

this._boundingSphere = computeBoundingSphere(this);
this._initialRadius = this._boundingSphere.radius;
this._updatedGltfVersion = true;
}
}

if (this._updatedGltfVersion && loadResources.pendingShaderLoads === 0) {
createResources(this, frameState);
}
}

if (loadResources.finished() ||
(incrementallyLoadTextures && loadResources.finishedEverythingButTextureCreation())) {
this._state = ModelState.LOADED;
Expand Down
12 changes: 10 additions & 2 deletions Source/Scene/ModelLoadResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ define([
this.createUniformMaps = true;
this.createRuntimeNodes = true;

this.decoding = false;
this.primitivesToDecode = new Queue();
this.finishedDecoding = false;

this.skinnedNodesIds = [];
}

Expand Down Expand Up @@ -79,11 +83,15 @@ define([
(this.programsToCreate.length === 0) &&
(this.pendingBufferViewToImage === 0);

return finishedPendingLoads && finishedResourceCreation;
return this.decodingComplete() && finishedPendingLoads && finishedResourceCreation;
};

ModelLoadResources.prototype.decodingComplete = function() {
return !this.decoding || (this.primitivesToDecode.length === 0 && this.finishedDecoding);
};

ModelLoadResources.prototype.finished = function() {
return this.finishedTextureCreation() && this.finishedEverythingButTextureCreation();
return this.decodingComplete() && this.finishedTextureCreation() && this.finishedEverythingButTextureCreation();
};

return ModelLoadResources;
Expand Down
Loading