import arraySlice from "../Core/arraySlice.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import defined from "../Core/defined.js"; import FeatureDetection from "../Core/FeatureDetection.js"; import TaskProcessor from "../Core/TaskProcessor.js"; import ForEach from "./GltfPipeline/ForEach.js"; /** * @private */ function DracoLoader() {} // Maximum concurrency to use when decoding draco models DracoLoader._maxDecodingConcurrency = Math.max( FeatureDetection.hardwareConcurrency - 1, 1 ); // Exposed for testing purposes DracoLoader._decoderTaskProcessor = undefined; DracoLoader._taskProcessorReady = false; DracoLoader._getDecoderTaskProcessor = function () { if (!defined(DracoLoader._decoderTaskProcessor)) { const processor = new TaskProcessor( "decodeDraco", DracoLoader._maxDecodingConcurrency ); processor .initWebAssemblyModule({ modulePath: "ThirdParty/Workers/draco_decoder_nodejs.js", wasmBinaryFile: "ThirdParty/draco_decoder.wasm", }) .then(function () { DracoLoader._taskProcessorReady = true; }); DracoLoader._decoderTaskProcessor = processor; } return DracoLoader._decoderTaskProcessor; }; /** * Returns true if the model uses or requires KHR_draco_mesh_compression. * * @private */ DracoLoader.hasExtension = function (model) { return ( defined(model.extensionsRequired.KHR_draco_mesh_compression) || defined(model.extensionsUsed.KHR_draco_mesh_compression) ); }; function addBufferToLoadResources(loadResources, typedArray) { // Create a new id to differentiate from original glTF bufferViews const bufferViewId = `runtime.${ Object.keys(loadResources.createdBufferViews).length }`; const loadResourceBuffers = loadResources.buffers; const id = Object.keys(loadResourceBuffers).length; loadResourceBuffers[id] = typedArray; loadResources.createdBufferViews[bufferViewId] = { buffer: id, byteOffset: 0, byteLength: typedArray.byteLength, }; return bufferViewId; } function addNewVertexBuffer(typedArray, model, context) { const loadResources = model._loadResources; const id = addBufferToLoadResources(loadResources, typedArray); loadResources.vertexBuffersToCreate.enqueue(id); return id; } function addNewIndexBuffer(indexArray, model, context) { const typedArray = indexArray.typedArray; const loadResources = model._loadResources; const id = addBufferToLoadResources(loadResources, typedArray); loadResources.indexBuffersToCreate.enqueue({ id: id, componentType: ComponentDatatype.fromTypedArray(typedArray), }); return { bufferViewId: id, numberOfIndices: indexArray.numberOfIndices, }; } function scheduleDecodingTask( decoderTaskProcessor, model, loadResources, context ) { if (!DracoLoader._taskProcessorReady) { // The task processor is not ready to schedule tasks return; } const taskData = loadResources.primitivesToDecode.peek(); if (!defined(taskData)) { // All primitives are processing return; } const promise = decoderTaskProcessor.scheduleTask(taskData, [ taskData.array.buffer, ]); if (!defined(promise)) { // Cannot schedule another task this frame return; } loadResources.activeDecodingTasks++; loadResources.primitivesToDecode.dequeue(); return promise.then(function (result) { loadResources.activeDecodingTasks--; const decodedIndexBuffer = addNewIndexBuffer( result.indexArray, model, context ); const attributes = {}; const decodedAttributeData = result.attributeData; for (const attributeName in decodedAttributeData) { if (decodedAttributeData.hasOwnProperty(attributeName)) { const attribute = decodedAttributeData[attributeName]; const vertexArray = attribute.array; const vertexBufferView = addNewVertexBuffer( vertexArray, model, context ); const data = attribute.data; data.bufferView = vertexBufferView; attributes[attributeName] = data; } } model._decodedData[`${taskData.mesh}.primitive.${taskData.primitive}`] = { bufferView: decodedIndexBuffer.bufferViewId, numberOfIndices: decodedIndexBuffer.numberOfIndices, attributes: attributes, }; }); } DracoLoader._decodedModelResourceCache = undefined; /** * Parses draco extension on model primitives and * adds the decoding data to the model's load resources. * * @private */ DracoLoader.parse = function (model, context) { if (!DracoLoader.hasExtension(model)) { return; } const loadResources = model._loadResources; const cacheKey = model.cacheKey; if (defined(cacheKey)) { if (!defined(DracoLoader._decodedModelResourceCache)) { if (!defined(context.cache.modelDecodingCache)) { context.cache.modelDecodingCache = {}; } DracoLoader._decodedModelResourceCache = context.cache.modelDecodingCache; } // Decoded data for model will be loaded from cache const cachedData = DracoLoader._decodedModelResourceCache[cacheKey]; if (defined(cachedData)) { cachedData.count++; loadResources.pendingDecodingCache = true; return; } } const dequantizeInShader = model._dequantizeInShader; const gltf = model.gltf; ForEach.mesh(gltf, function (mesh, meshId) { ForEach.meshPrimitive(mesh, function (primitive, primitiveId) { if (!defined(primitive.extensions)) { return; } const compressionData = primitive.extensions.KHR_draco_mesh_compression; if (!defined(compressionData)) { return; } const bufferView = gltf.bufferViews[compressionData.bufferView]; const typedArray = arraySlice( gltf.buffers[bufferView.buffer].extras._pipeline.source, bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength ); loadResources.primitivesToDecode.enqueue({ mesh: meshId, primitive: primitiveId, array: typedArray, bufferView: bufferView, compressedAttributes: compressionData.attributes, dequantizeInShader: dequantizeInShader, }); }); }); }; /** * Schedules decoding tasks available this frame. * @private */ DracoLoader.decodeModel = function (model, context) { if (!DracoLoader.hasExtension(model)) { return Promise.resolve(); } const loadResources = model._loadResources; const cacheKey = model.cacheKey; if (defined(cacheKey) && defined(DracoLoader._decodedModelResourceCache)) { const cachedData = DracoLoader._decodedModelResourceCache[cacheKey]; // Load decoded data for model when cache is ready if (defined(cachedData) && loadResources.pendingDecodingCache) { return Promise.resolve(cachedData.ready).then(function () { model._decodedData = cachedData.data; loadResources.pendingDecodingCache = false; }); } // Decoded data for model should be cached when ready DracoLoader._decodedModelResourceCache[cacheKey] = { ready: false, count: 1, data: undefined, }; } if (loadResources.primitivesToDecode.length === 0) { // No more tasks to schedule return Promise.resolve(); } const decoderTaskProcessor = DracoLoader._getDecoderTaskProcessor(); const decodingPromises = []; let promise = scheduleDecodingTask( decoderTaskProcessor, model, loadResources, context ); while (defined(promise)) { decodingPromises.push(promise); promise = scheduleDecodingTask( decoderTaskProcessor, model, loadResources, context ); } return Promise.all(decodingPromises); }; /** * Decodes a compressed point cloud. Returns undefined if the task cannot be scheduled. * @private */ DracoLoader.decodePointCloud = function (parameters) { const decoderTaskProcessor = DracoLoader._getDecoderTaskProcessor(); if (!DracoLoader._taskProcessorReady) { // The task processor is not ready to schedule tasks return; } return decoderTaskProcessor.scheduleTask(parameters, [ parameters.buffer.buffer, ]); }; /** * Decodes a buffer view. Returns undefined if the task cannot be scheduled. * * @param {Object} options Object with the following properties: * @param {Uint8Array} options.array The typed array containing the buffer view data. * @param {Object} options.bufferView The glTF buffer view object. * @param {Object.} options.compressedAttributes The compressed attributes. * @param {Boolean} options.dequantizeInShader Whether POSITION and NORMAL attributes should be dequantized on the GPU. * * @returns {Promise} A promise that resolves to the decoded indices and attributes. * @private */ DracoLoader.decodeBufferView = function (options) { const decoderTaskProcessor = DracoLoader._getDecoderTaskProcessor(); if (!DracoLoader._taskProcessorReady) { // The task processor is not ready to schedule tasks return; } return decoderTaskProcessor.scheduleTask(options, [options.array.buffer]); }; /** * Caches a models decoded data so it doesn't need to decode more than once. * @private */ DracoLoader.cacheDataForModel = function (model) { const cacheKey = model.cacheKey; if (defined(cacheKey) && defined(DracoLoader._decodedModelResourceCache)) { const cachedData = DracoLoader._decodedModelResourceCache[cacheKey]; if (defined(cachedData)) { cachedData.ready = true; cachedData.data = model._decodedData; } } }; /** * Destroys the cached data that this model references if it is no longer in use. * @private */ DracoLoader.destroyCachedDataForModel = function (model) { const cacheKey = model.cacheKey; if (defined(cacheKey) && defined(DracoLoader._decodedModelResourceCache)) { const cachedData = DracoLoader._decodedModelResourceCache[cacheKey]; if (defined(cachedData) && --cachedData.count === 0) { delete DracoLoader._decodedModelResourceCache[cacheKey]; } } }; export default DracoLoader;