DRACOLoader.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import { BufferAttribute, BufferGeometry, FileLoader, Loader } from '../lib/three.module.js'
  2. const _taskCache = new WeakMap()
  3. class DRACOLoader extends Loader {
  4. constructor(manager) {
  5. super(manager)
  6. this.decoderPath = ''
  7. this.decoderConfig = {}
  8. this.decoderBinary = null
  9. this.decoderPending = null
  10. this.workerLimit = 4
  11. this.workerPool = []
  12. this.workerNextTaskID = 1
  13. this.workerSourceURL = ''
  14. this.defaultAttributeIDs = {
  15. position: 'POSITION',
  16. normal: 'NORMAL',
  17. color: 'COLOR',
  18. uv: 'TEX_COORD'
  19. }
  20. this.defaultAttributeTypes = {
  21. position: 'Float32Array',
  22. normal: 'Float32Array',
  23. color: 'Float32Array',
  24. uv: 'Float32Array'
  25. }
  26. }
  27. setDecoderPath(path) {
  28. this.decoderPath = path
  29. return this
  30. }
  31. setDecoderConfig(config) {
  32. this.decoderConfig = config
  33. return this
  34. }
  35. setWorkerLimit(workerLimit) {
  36. this.workerLimit = workerLimit
  37. return this
  38. }
  39. load(url, onLoad, onProgress, onError) {
  40. const loader = new FileLoader(this.manager)
  41. loader.setPath(this.path)
  42. loader.setResponseType('arraybuffer')
  43. loader.setRequestHeader(this.requestHeader)
  44. loader.setWithCredentials(this.withCredentials)
  45. loader.load(
  46. url,
  47. buffer => {
  48. const taskConfig = {
  49. attributeIDs: this.defaultAttributeIDs,
  50. attributeTypes: this.defaultAttributeTypes,
  51. useUniqueIDs: false
  52. }
  53. this.decodeGeometry(buffer, taskConfig)
  54. .then(onLoad)
  55. .catch(onError)
  56. },
  57. onProgress,
  58. onError
  59. )
  60. }
  61. /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */
  62. decodeDracoFile(buffer, callback, attributeIDs, attributeTypes) {
  63. const taskConfig = {
  64. attributeIDs: attributeIDs || this.defaultAttributeIDs,
  65. attributeTypes: attributeTypes || this.defaultAttributeTypes,
  66. useUniqueIDs: !!attributeIDs
  67. }
  68. this.decodeGeometry(buffer, taskConfig).then(callback)
  69. }
  70. decodeGeometry(buffer, taskConfig) {
  71. // TODO: For backward-compatibility, support 'attributeTypes' objects containing
  72. // references (rather than names) to typed array constructors. These must be
  73. // serialized before sending them to the worker.
  74. for (const attribute in taskConfig.attributeTypes) {
  75. const type = taskConfig.attributeTypes[attribute]
  76. if (type.BYTES_PER_ELEMENT !== undefined) {
  77. taskConfig.attributeTypes[attribute] = type.name
  78. }
  79. }
  80. //
  81. const taskKey = JSON.stringify(taskConfig)
  82. // Check for an existing task using this buffer. A transferred buffer cannot be transferred
  83. // again from this thread.
  84. if (_taskCache.has(buffer)) {
  85. const cachedTask = _taskCache.get(buffer)
  86. if (cachedTask.key === taskKey) {
  87. return cachedTask.promise
  88. } else if (buffer.byteLength === 0) {
  89. // Technically, it would be possible to wait for the previous task to complete,
  90. // transfer the buffer back, and decode again with the second configuration. That
  91. // is complex, and I don't know of any reason to decode a Draco buffer twice in
  92. // different ways, so this is left unimplemented.
  93. throw new Error('THREE.DRACOLoader: Unable to re-decode a buffer with different ' + 'settings. Buffer has already been transferred.')
  94. }
  95. }
  96. //
  97. let worker
  98. const taskID = this.workerNextTaskID++
  99. const taskCost = buffer.byteLength
  100. // Obtain a worker and assign a task, and construct a geometry instance
  101. // when the task completes.
  102. const geometryPending = this._getWorker(taskID, taskCost)
  103. .then(_worker => {
  104. worker = _worker
  105. return new Promise((resolve, reject) => {
  106. worker._callbacks[taskID] = { resolve, reject }
  107. worker.postMessage({ type: 'decode', id: taskID, taskConfig, buffer }, [buffer])
  108. // this.debug();
  109. })
  110. })
  111. .then(message => this._createGeometry(message.geometry))
  112. // Remove task from the task list.
  113. // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
  114. geometryPending
  115. .catch(() => true)
  116. .then(() => {
  117. if (worker && taskID) {
  118. this._releaseTask(worker, taskID)
  119. // this.debug();
  120. }
  121. })
  122. // Cache the task result.
  123. _taskCache.set(buffer, {
  124. key: taskKey,
  125. promise: geometryPending
  126. })
  127. return geometryPending
  128. }
  129. _createGeometry(geometryData) {
  130. const geometry = new BufferGeometry()
  131. if (geometryData.index) {
  132. geometry.setIndex(new BufferAttribute(geometryData.index.array, 1))
  133. }
  134. for (let i = 0; i < geometryData.attributes.length; i++) {
  135. const attribute = geometryData.attributes[i]
  136. const name = attribute.name
  137. const array = attribute.array
  138. const itemSize = attribute.itemSize
  139. geometry.setAttribute(name, new BufferAttribute(array, itemSize))
  140. }
  141. return geometry
  142. }
  143. _loadLibrary(url, responseType) {
  144. const loader = new FileLoader(this.manager)
  145. loader.setPath(this.decoderPath)
  146. loader.setResponseType(responseType)
  147. loader.setWithCredentials(this.withCredentials)
  148. return new Promise((resolve, reject) => {
  149. loader.load(url, resolve, undefined, reject)
  150. })
  151. }
  152. preload() {
  153. this._initDecoder()
  154. return this
  155. }
  156. _initDecoder() {
  157. if (this.decoderPending) return this.decoderPending
  158. const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'
  159. const librariesPending = []
  160. if (useJS) {
  161. librariesPending.push(this._loadLibrary('draco_decoder.js', 'text'))
  162. } else {
  163. librariesPending.push(this._loadLibrary('draco_wasm_wrapper.js', 'text'))
  164. librariesPending.push(this._loadLibrary('draco_decoder.wasm', 'arraybuffer'))
  165. }
  166. this.decoderPending = Promise.all(librariesPending).then(libraries => {
  167. const jsContent = libraries[0]
  168. if (!useJS) {
  169. this.decoderConfig.wasmBinary = libraries[1]
  170. }
  171. const fn = DRACOWorker.toString()
  172. const body = ['/* draco decoder */', jsContent, '', '/* worker */', fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}'))].join('\n')
  173. this.workerSourceURL = URL.createObjectURL(new Blob([body]))
  174. })
  175. return this.decoderPending
  176. }
  177. _getWorker(taskID, taskCost) {
  178. return this._initDecoder().then(() => {
  179. if (this.workerPool.length < this.workerLimit) {
  180. const worker = new Worker(this.workerSourceURL)
  181. worker._callbacks = {}
  182. worker._taskCosts = {}
  183. worker._taskLoad = 0
  184. worker.postMessage({ type: 'init', decoderConfig: this.decoderConfig })
  185. worker.onmessage = function(e) {
  186. const message = e.data
  187. switch (message.type) {
  188. case 'decode':
  189. worker._callbacks[message.id].resolve(message)
  190. break
  191. case 'error':
  192. worker._callbacks[message.id].reject(message)
  193. break
  194. default:
  195. console.error('THREE.DRACOLoader: Unexpected message, "' + message.type + '"')
  196. }
  197. }
  198. this.workerPool.push(worker)
  199. } else {
  200. this.workerPool.sort(function(a, b) {
  201. return a._taskLoad > b._taskLoad ? -1 : 1
  202. })
  203. }
  204. const worker = this.workerPool[this.workerPool.length - 1]
  205. worker._taskCosts[taskID] = taskCost
  206. worker._taskLoad += taskCost
  207. return worker
  208. })
  209. }
  210. _releaseTask(worker, taskID) {
  211. worker._taskLoad -= worker._taskCosts[taskID]
  212. delete worker._callbacks[taskID]
  213. delete worker._taskCosts[taskID]
  214. }
  215. debug() {
  216. console.log(
  217. 'Task load: ',
  218. this.workerPool.map(worker => worker._taskLoad)
  219. )
  220. }
  221. dispose() {
  222. for (let i = 0; i < this.workerPool.length; ++i) {
  223. this.workerPool[i].terminate()
  224. }
  225. this.workerPool.length = 0
  226. return this
  227. }
  228. }
  229. /* WEB WORKER */
  230. function DRACOWorker() {
  231. let decoderConfig
  232. let decoderPending
  233. onmessage = function(e) {
  234. const message = e.data
  235. switch (message.type) {
  236. case 'init':
  237. decoderConfig = message.decoderConfig
  238. decoderPending = new Promise(function(resolve /*, reject*/) {
  239. decoderConfig.onModuleLoaded = function(draco) {
  240. // Module is Promise-like. Wrap before resolving to avoid loop.
  241. resolve({ draco: draco })
  242. }
  243. DracoDecoderModule(decoderConfig) // eslint-disable-line no-undef
  244. })
  245. break
  246. case 'decode':
  247. const buffer = message.buffer
  248. const taskConfig = message.taskConfig
  249. decoderPending.then(module => {
  250. const draco = module.draco
  251. const decoder = new draco.Decoder()
  252. const decoderBuffer = new draco.DecoderBuffer()
  253. decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength)
  254. try {
  255. const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig)
  256. const buffers = geometry.attributes.map(attr => attr.array.buffer)
  257. if (geometry.index) buffers.push(geometry.index.array.buffer)
  258. self.postMessage({ type: 'decode', id: message.id, geometry }, buffers)
  259. } catch (error) {
  260. console.error(error)
  261. self.postMessage({ type: 'error', id: message.id, error: error.message })
  262. } finally {
  263. draco.destroy(decoderBuffer)
  264. draco.destroy(decoder)
  265. }
  266. })
  267. break
  268. }
  269. }
  270. function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) {
  271. const attributeIDs = taskConfig.attributeIDs
  272. const attributeTypes = taskConfig.attributeTypes
  273. let dracoGeometry
  274. let decodingStatus
  275. const geometryType = decoder.GetEncodedGeometryType(decoderBuffer)
  276. if (geometryType === draco.TRIANGULAR_MESH) {
  277. dracoGeometry = new draco.Mesh()
  278. decodingStatus = decoder.DecodeBufferToMesh(decoderBuffer, dracoGeometry)
  279. } else if (geometryType === draco.POINT_CLOUD) {
  280. dracoGeometry = new draco.PointCloud()
  281. decodingStatus = decoder.DecodeBufferToPointCloud(decoderBuffer, dracoGeometry)
  282. } else {
  283. throw new Error('THREE.DRACOLoader: Unexpected geometry type.')
  284. }
  285. if (!decodingStatus.ok() || dracoGeometry.ptr === 0) {
  286. throw new Error('THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg())
  287. }
  288. const geometry = { index: null, attributes: [] }
  289. // Gather all vertex attributes.
  290. for (const attributeName in attributeIDs) {
  291. const attributeType = self[attributeTypes[attributeName]]
  292. let attribute
  293. let attributeID
  294. // A Draco file may be created with default vertex attributes, whose attribute IDs
  295. // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
  296. // a Draco file may contain a custom set of attributes, identified by known unique
  297. // IDs. glTF files always do the latter, and `.drc` files typically do the former.
  298. if (taskConfig.useUniqueIDs) {
  299. attributeID = attributeIDs[attributeName]
  300. attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID)
  301. } else {
  302. attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]])
  303. if (attributeID === -1) continue
  304. attribute = decoder.GetAttribute(dracoGeometry, attributeID)
  305. }
  306. geometry.attributes.push(decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute))
  307. }
  308. // Add index.
  309. if (geometryType === draco.TRIANGULAR_MESH) {
  310. geometry.index = decodeIndex(draco, decoder, dracoGeometry)
  311. }
  312. draco.destroy(dracoGeometry)
  313. return geometry
  314. }
  315. function decodeIndex(draco, decoder, dracoGeometry) {
  316. const numFaces = dracoGeometry.num_faces()
  317. const numIndices = numFaces * 3
  318. const byteLength = numIndices * 4
  319. const ptr = draco._malloc(byteLength)
  320. decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr)
  321. const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice()
  322. draco._free(ptr)
  323. return { array: index, itemSize: 1 }
  324. }
  325. function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
  326. const numComponents = attribute.num_components()
  327. const numPoints = dracoGeometry.num_points()
  328. const numValues = numPoints * numComponents
  329. const byteLength = numValues * attributeType.BYTES_PER_ELEMENT
  330. const dataType = getDracoDataType(draco, attributeType)
  331. const ptr = draco._malloc(byteLength)
  332. decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr)
  333. const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice()
  334. draco._free(ptr)
  335. return {
  336. name: attributeName,
  337. array: array,
  338. itemSize: numComponents
  339. }
  340. }
  341. function getDracoDataType(draco, attributeType) {
  342. switch (attributeType) {
  343. case Float32Array:
  344. return draco.DT_FLOAT32
  345. case Int8Array:
  346. return draco.DT_INT8
  347. case Int16Array:
  348. return draco.DT_INT16
  349. case Int32Array:
  350. return draco.DT_INT32
  351. case Uint8Array:
  352. return draco.DT_UINT8
  353. case Uint16Array:
  354. return draco.DT_UINT16
  355. case Uint32Array:
  356. return draco.DT_UINT32
  357. }
  358. }
  359. }
  360. export { DRACOLoader }