UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

400 lines (386 loc) 14.5 kB
import { BufferAttribute, BufferGeometry, Color, ColorManagement, FileLoader, Loader, LinearSRGBColorSpace, SRGBColorSpace } from 'three'; const _taskCache = new WeakMap(); class DRACOLoader extends Loader { constructor(manager) { super(manager); this.decoderPath = ''; this.decoderConfig = {}; this.decoderBinary = null; this.decoderPending = null; this.workerLimit = 4; this.workerPool = []; this.workerNextTaskID = 1; this.workerSourceURL = ''; this.defaultAttributeIDs = { position: 'POSITION', normal: 'NORMAL', color: 'COLOR', uv: 'TEX_COORD' }; this.defaultAttributeTypes = { position: 'Float32Array', normal: 'Float32Array', color: 'Float32Array', uv: 'Float32Array' }; } setDecoderPath(path) { this.decoderPath = path; return this; } setDecoderConfig(config) { this.decoderConfig = config; return this; } setWorkerLimit(workerLimit) { this.workerLimit = workerLimit; return this; } load(url, onLoad, onProgress, onError) { const loader = new FileLoader(this.manager); loader.setPath(this.path); loader.setResponseType('arraybuffer'); loader.setRequestHeader(this.requestHeader); loader.setWithCredentials(this.withCredentials); loader.load(url, buffer => { this.parse(buffer, onLoad, onError); }, onProgress, onError); } parse(buffer, onLoad) { let onError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : () => {}; this.decodeDracoFile(buffer, onLoad, null, null, SRGBColorSpace, onError).catch(onError); } decodeDracoFile(buffer, callback, attributeIDs, attributeTypes) { let vertexColorSpace = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : LinearSRGBColorSpace; let onError = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : () => {}; const taskConfig = { attributeIDs: attributeIDs || this.defaultAttributeIDs, attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !!attributeIDs, vertexColorSpace: vertexColorSpace }; return this.decodeGeometry(buffer, taskConfig).then(callback).catch(onError); } decodeGeometry(buffer, taskConfig) { const taskKey = JSON.stringify(taskConfig); // Check for an existing task using this buffer. A transferred buffer cannot be transferred // again from this thread. if (_taskCache.has(buffer)) { const cachedTask = _taskCache.get(buffer); if (cachedTask.key === taskKey) { return cachedTask.promise; } else if (buffer.byteLength === 0) { // Technically, it would be possible to wait for the previous task to complete, // transfer the buffer back, and decode again with the second configuration. That // is complex, and I don't know of any reason to decode a Draco buffer twice in // different ways, so this is left unimplemented. throw new Error('THREE.DRACOLoader: Unable to re-decode a buffer with different ' + 'settings. Buffer has already been transferred.'); } } // let worker; const taskID = this.workerNextTaskID++; const taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance // when the task completes. const geometryPending = this._getWorker(taskID, taskCost).then(_worker => { worker = _worker; return new Promise((resolve, reject) => { worker._callbacks[taskID] = { resolve, reject }; worker.postMessage({ type: 'decode', id: taskID, taskConfig, buffer }, [buffer]); // this.debug(); }); }).then(message => this._createGeometry(message.geometry)); // Remove task from the task list. // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) geometryPending.catch(() => true).then(() => { if (worker && taskID) { this._releaseTask(worker, taskID); // this.debug(); } }); // Cache the task result. _taskCache.set(buffer, { key: taskKey, promise: geometryPending }); return geometryPending; } _createGeometry(geometryData) { const geometry = new BufferGeometry(); if (geometryData.index) { geometry.setIndex(new BufferAttribute(geometryData.index.array, 1)); } for (let i = 0; i < geometryData.attributes.length; i++) { const result = geometryData.attributes[i]; const name = result.name; const array = result.array; const itemSize = result.itemSize; const attribute = new BufferAttribute(array, itemSize); if (name === 'color') { this._assignVertexColorSpace(attribute, result.vertexColorSpace); attribute.normalized = array instanceof Float32Array === false; } geometry.setAttribute(name, attribute); } return geometry; } _assignVertexColorSpace(attribute, inputColorSpace) { // While .drc files do not specify colorspace, the only 'official' tooling // is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc // file is passed into .load() or .parse(). GLTFLoader uses internal APIs // to decode geometry, and vertex colors are already Linear-sRGB in there. if (inputColorSpace !== SRGBColorSpace) return; const _color = new Color(); for (let i = 0, il = attribute.count; i < il; i++) { _color.fromBufferAttribute(attribute, i); ColorManagement.toWorkingColorSpace(_color, SRGBColorSpace); attribute.setXYZ(i, _color.r, _color.g, _color.b); } } _loadLibrary(url, responseType) { const loader = new FileLoader(this.manager); loader.setPath(this.decoderPath); loader.setResponseType(responseType); loader.setWithCredentials(this.withCredentials); return new Promise((resolve, reject) => { loader.load(url, resolve, undefined, reject); }); } preload() { this._initDecoder(); return this; } _initDecoder() { if (this.decoderPending) return this.decoderPending; const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; const librariesPending = []; if (useJS) { librariesPending.push(this._loadLibrary('draco_decoder.js', 'text')); } else { librariesPending.push(this._loadLibrary('draco_wasm_wrapper.js', 'text')); librariesPending.push(this._loadLibrary('draco_decoder.wasm', 'arraybuffer')); } this.decoderPending = Promise.all(librariesPending).then(libraries => { const jsContent = libraries[0]; if (!useJS) { this.decoderConfig.wasmBinary = libraries[1]; } const fn = DRACOWorker.toString(); const body = ['/* draco decoder */', jsContent, '', '/* worker */', fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}'))].join('\n'); this.workerSourceURL = URL.createObjectURL(new Blob([body])); }); return this.decoderPending; } _getWorker(taskID, taskCost) { return this._initDecoder().then(() => { if (this.workerPool.length < this.workerLimit) { const worker = new Worker(this.workerSourceURL); worker._callbacks = {}; worker._taskCosts = {}; worker._taskLoad = 0; worker.postMessage({ type: 'init', decoderConfig: this.decoderConfig }); worker.onmessage = function (e) { const message = e.data; switch (message.type) { case 'decode': worker._callbacks[message.id].resolve(message); break; case 'error': worker._callbacks[message.id].reject(message); break; default: console.error('THREE.DRACOLoader: Unexpected message, "' + message.type + '"'); } }; this.workerPool.push(worker); } else { this.workerPool.sort(function (a, b) { return a._taskLoad > b._taskLoad ? -1 : 1; }); } const worker = this.workerPool[this.workerPool.length - 1]; worker._taskCosts[taskID] = taskCost; worker._taskLoad += taskCost; return worker; }); } _releaseTask(worker, taskID) { worker._taskLoad -= worker._taskCosts[taskID]; delete worker._callbacks[taskID]; delete worker._taskCosts[taskID]; } debug() { console.log('Task load: ', this.workerPool.map(worker => worker._taskLoad)); } dispose() { for (let i = 0; i < this.workerPool.length; ++i) { this.workerPool[i].terminate(); } this.workerPool.length = 0; if (this.workerSourceURL !== '') { URL.revokeObjectURL(this.workerSourceURL); } return this; } } /* WEB WORKER */ function DRACOWorker() { let decoderConfig; let decoderPending; onmessage = function (e) { const message = e.data; switch (message.type) { case 'init': decoderConfig = message.decoderConfig; decoderPending = new Promise(function (resolve /*, reject*/) { decoderConfig.onModuleLoaded = function (draco) { // Module is Promise-like. Wrap before resolving to avoid loop. resolve({ draco: draco }); }; DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef }); break; case 'decode': const buffer = message.buffer; const taskConfig = message.taskConfig; decoderPending.then(module => { const draco = module.draco; const decoder = new draco.Decoder(); try { const geometry = decodeGeometry(draco, decoder, new Int8Array(buffer), taskConfig); const buffers = geometry.attributes.map(attr => attr.array.buffer); if (geometry.index) buffers.push(geometry.index.array.buffer); self.postMessage({ type: 'decode', id: message.id, geometry }, buffers); } catch (error) { console.error(error); self.postMessage({ type: 'error', id: message.id, error: error.message }); } finally { draco.destroy(decoder); } }); break; } }; function decodeGeometry(draco, decoder, array, taskConfig) { const attributeIDs = taskConfig.attributeIDs; const attributeTypes = taskConfig.attributeTypes; let dracoGeometry; let decodingStatus; const geometryType = decoder.GetEncodedGeometryType(array); if (geometryType === draco.TRIANGULAR_MESH) { dracoGeometry = new draco.Mesh(); decodingStatus = decoder.DecodeArrayToMesh(array, array.byteLength, dracoGeometry); } else if (geometryType === draco.POINT_CLOUD) { dracoGeometry = new draco.PointCloud(); decodingStatus = decoder.DecodeArrayToPointCloud(array, array.byteLength, dracoGeometry); } else { throw new Error('THREE.DRACOLoader: Unexpected geometry type.'); } if (!decodingStatus.ok() || dracoGeometry.ptr === 0) { throw new Error('THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg()); } const geometry = { index: null, attributes: [] }; // Gather all vertex attributes. for (const attributeName in attributeIDs) { const attributeType = self[attributeTypes[attributeName]]; let attribute; let attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, // a Draco file may contain a custom set of attributes, identified by known unique // IDs. glTF files always do the latter, and `.drc` files typically do the former. if (taskConfig.useUniqueIDs) { attributeID = attributeIDs[attributeName]; attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID); } else { attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]]); if (attributeID === -1) continue; attribute = decoder.GetAttribute(dracoGeometry, attributeID); } const attributeResult = decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute); if (attributeName === 'color') { attributeResult.vertexColorSpace = taskConfig.vertexColorSpace; } geometry.attributes.push(attributeResult); } // Add index. if (geometryType === draco.TRIANGULAR_MESH) { geometry.index = decodeIndex(draco, decoder, dracoGeometry); } draco.destroy(dracoGeometry); return geometry; } function decodeIndex(draco, decoder, dracoGeometry) { const numFaces = dracoGeometry.num_faces(); const numIndices = numFaces * 3; const byteLength = numIndices * 4; const ptr = draco._malloc(byteLength); decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr); const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice(); draco._free(ptr); return { array: index, itemSize: 1 }; } function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) { const numComponents = attribute.num_components(); const numPoints = dracoGeometry.num_points(); const numValues = numPoints * numComponents; const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; const dataType = getDracoDataType(draco, attributeType); const ptr = draco._malloc(byteLength); decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr); const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice(); draco._free(ptr); return { name: attributeName, array: array, itemSize: numComponents }; } function getDracoDataType(draco, attributeType) { switch (attributeType) { case Float32Array: return draco.DT_FLOAT32; case Int8Array: return draco.DT_INT8; case Int16Array: return draco.DT_INT16; case Int32Array: return draco.DT_INT32; case Uint8Array: return draco.DT_UINT8; case Uint16Array: return draco.DT_UINT16; case Uint32Array: return draco.DT_UINT32; } } } export { DRACOLoader };