dracoCompression.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /* eslint-disable @typescript-eslint/naming-convention */
  2. import { Tools } from "../../Misc/tools.js";
  3. import { AutoReleaseWorkerPool } from "../../Misc/workerPool.js";
  4. import { Geometry } from "../geometry.js";
  5. import { VertexBuffer } from "../buffer.js";
  6. import { VertexData } from "../mesh.vertexData.js";
  7. import { Logger } from "../../Misc/logger.js";
  8. import { decodeMesh, workerFunction, initializeWebWorker } from "./dracoCompressionWorker.js";
  9. function createDecoderAsync(wasmBinary, jsModule) {
  10. return new Promise((resolve) => {
  11. (jsModule || DracoDecoderModule)({ wasmBinary }).then((module) => {
  12. resolve({ module });
  13. });
  14. });
  15. }
  16. /**
  17. * Draco compression (https://google.github.io/draco/)
  18. *
  19. * This class wraps the Draco module.
  20. *
  21. * **Encoder**
  22. *
  23. * The encoder is not currently implemented.
  24. *
  25. * **Decoder**
  26. *
  27. * By default, the configuration points to a copy of the Draco decoder files for glTF from the babylon.js preview cdn https://preview.babylonjs.com/draco_wasm_wrapper_gltf.js.
  28. *
  29. * To update the configuration, use the following code:
  30. * ```javascript
  31. * DracoCompression.Configuration = {
  32. * decoder: {
  33. * wasmUrl: "<url to the WebAssembly library>",
  34. * wasmBinaryUrl: "<url to the WebAssembly binary>",
  35. * fallbackUrl: "<url to the fallback JavaScript library>",
  36. * }
  37. * };
  38. * ```
  39. *
  40. * Draco has two versions, one for WebAssembly and one for JavaScript. The decoder configuration can be set to only support WebAssembly or only support the JavaScript version.
  41. * Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
  42. * Use `DracoCompression.DecoderAvailable` to determine if the decoder configuration is available for the current context.
  43. *
  44. * To decode Draco compressed data, get the default DracoCompression object and call decodeMeshToGeometryAsync:
  45. * ```javascript
  46. * var geometry = await DracoCompression.Default.decodeMeshToGeometryAsync(data);
  47. * ```
  48. *
  49. * @see https://playground.babylonjs.com/#DMZIBD#0
  50. */
  51. export class DracoCompression {
  52. /**
  53. * Returns true if the decoder configuration is available.
  54. */
  55. static get DecoderAvailable() {
  56. const decoder = DracoCompression.Configuration.decoder;
  57. return !!((decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") || decoder.fallbackUrl);
  58. }
  59. static GetDefaultNumWorkers() {
  60. if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
  61. return 1;
  62. }
  63. // Use 50% of the available logical processors but capped at 4.
  64. return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
  65. }
  66. /**
  67. * Default instance for the draco compression object.
  68. */
  69. static get Default() {
  70. if (!DracoCompression._Default) {
  71. DracoCompression._Default = new DracoCompression();
  72. }
  73. return DracoCompression._Default;
  74. }
  75. /**
  76. * Constructor
  77. * @param numWorkers The number of workers for async operations Or an options object. Specify `0` to disable web workers and run synchronously in the current context.
  78. */
  79. constructor(numWorkers = DracoCompression.DefaultNumWorkers) {
  80. const decoder = DracoCompression.Configuration.decoder;
  81. // check if the decoder binary and worker pool was injected
  82. // Note - it is expected that the developer checked if WebWorker, WebAssembly and the URL object are available
  83. if (decoder.workerPool || (typeof numWorkers === "object" && numWorkers.workerPool)) {
  84. // set the promise accordingly
  85. this._workerPoolPromise = Promise.resolve(decoder.workerPool || numWorkers.workerPool);
  86. }
  87. else {
  88. // to avoid making big changes to the decider, if wasmBinary is provided use it in the wasmBinaryPromise
  89. const wasmBinaryProvided = decoder.wasmBinary || (typeof numWorkers === "object" && numWorkers.wasmBinary);
  90. const numberOfWorkers = typeof numWorkers === "number" ? numWorkers : numWorkers.numWorkers;
  91. const useWorkers = numberOfWorkers && typeof Worker === "function" && typeof URL === "function";
  92. const urlNeeded = useWorkers || (!useWorkers && !decoder.jsModule);
  93. // code maintained here for back-compat with no changes
  94. const decoderInfo = decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object"
  95. ? {
  96. url: urlNeeded ? Tools.GetBabylonScriptURL(decoder.wasmUrl, true) : "",
  97. wasmBinaryPromise: wasmBinaryProvided ? Promise.resolve(wasmBinaryProvided) : Tools.LoadFileAsync(Tools.GetBabylonScriptURL(decoder.wasmBinaryUrl, true)),
  98. }
  99. : {
  100. url: urlNeeded ? Tools.GetBabylonScriptURL(decoder.fallbackUrl) : "",
  101. wasmBinaryPromise: Promise.resolve(undefined),
  102. };
  103. if (useWorkers) {
  104. this._workerPoolPromise = decoderInfo.wasmBinaryPromise.then((decoderWasmBinary) => {
  105. const workerContent = `${decodeMesh}(${workerFunction})()`;
  106. const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
  107. return new AutoReleaseWorkerPool(numberOfWorkers, () => {
  108. const worker = new Worker(workerBlobUrl);
  109. return initializeWebWorker(worker, decoderWasmBinary, decoderInfo.url);
  110. });
  111. });
  112. }
  113. else {
  114. this._decoderModulePromise = decoderInfo.wasmBinaryPromise.then(async (decoderWasmBinary) => {
  115. if (typeof DracoDecoderModule === "undefined") {
  116. if (!decoder.jsModule) {
  117. if (!decoderInfo.url) {
  118. throw new Error("Draco decoder module is not available");
  119. }
  120. await Tools.LoadBabylonScriptAsync(decoderInfo.url);
  121. }
  122. }
  123. return await createDecoderAsync(decoderWasmBinary, decoder.jsModule);
  124. });
  125. }
  126. }
  127. }
  128. /**
  129. * Stop all async operations and release resources.
  130. */
  131. dispose() {
  132. if (this._workerPoolPromise) {
  133. this._workerPoolPromise.then((workerPool) => {
  134. workerPool.dispose();
  135. });
  136. }
  137. delete this._workerPoolPromise;
  138. delete this._decoderModulePromise;
  139. }
  140. /**
  141. * Returns a promise that resolves when ready. Call this manually to ensure draco compression is ready before use.
  142. * @returns a promise that resolves when ready
  143. */
  144. async whenReadyAsync() {
  145. if (this._workerPoolPromise) {
  146. await this._workerPoolPromise;
  147. return;
  148. }
  149. if (this._decoderModulePromise) {
  150. await this._decoderModulePromise;
  151. return;
  152. }
  153. }
  154. /**
  155. * Decode Draco compressed mesh data to mesh data.
  156. * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
  157. * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
  158. * @param gltfNormalizedOverride A map of attributes from vertex buffer kinds to normalized flags to override the Draco normalization
  159. * @returns A promise that resolves with the decoded mesh data
  160. */
  161. decodeMeshToMeshDataAsync(data, attributes, gltfNormalizedOverride) {
  162. const dataView = data instanceof ArrayBuffer ? new Int8Array(data) : new Int8Array(data.buffer, data.byteOffset, data.byteLength);
  163. const applyGltfNormalizedOverride = (kind, normalized) => {
  164. if (gltfNormalizedOverride && gltfNormalizedOverride[kind] !== undefined) {
  165. if (normalized !== gltfNormalizedOverride[kind]) {
  166. Logger.Warn(`Normalized flag from Draco data (${normalized}) does not match normalized flag from glTF accessor (${gltfNormalizedOverride[kind]}). Using flag from glTF accessor.`);
  167. }
  168. return gltfNormalizedOverride[kind];
  169. }
  170. else {
  171. return normalized;
  172. }
  173. };
  174. if (this._workerPoolPromise) {
  175. return this._workerPoolPromise.then((workerPool) => {
  176. return new Promise((resolve, reject) => {
  177. workerPool.push((worker, onComplete) => {
  178. let resultIndices = null;
  179. const resultAttributes = [];
  180. const onError = (error) => {
  181. worker.removeEventListener("error", onError);
  182. worker.removeEventListener("message", onMessage);
  183. reject(error);
  184. onComplete();
  185. };
  186. const onMessage = (event) => {
  187. const message = event.data;
  188. switch (message.id) {
  189. case "decodeMeshDone": {
  190. worker.removeEventListener("error", onError);
  191. worker.removeEventListener("message", onMessage);
  192. resolve({ indices: resultIndices, attributes: resultAttributes, totalVertices: message.totalVertices });
  193. onComplete();
  194. break;
  195. }
  196. case "indices": {
  197. resultIndices = message.data;
  198. break;
  199. }
  200. case "attribute": {
  201. resultAttributes.push({
  202. kind: message.kind,
  203. data: message.data,
  204. size: message.size,
  205. byteOffset: message.byteOffset,
  206. byteStride: message.byteStride,
  207. normalized: applyGltfNormalizedOverride(message.kind, message.normalized),
  208. });
  209. break;
  210. }
  211. }
  212. };
  213. worker.addEventListener("error", onError);
  214. worker.addEventListener("message", onMessage);
  215. const dataViewCopy = dataView.slice();
  216. worker.postMessage({ id: "decodeMesh", dataView: dataViewCopy, attributes: attributes }, [dataViewCopy.buffer]);
  217. });
  218. });
  219. });
  220. }
  221. if (this._decoderModulePromise) {
  222. return this._decoderModulePromise.then((decoder) => {
  223. let resultIndices = null;
  224. const resultAttributes = [];
  225. const numPoints = decodeMesh(decoder.module, dataView, attributes, (indices) => {
  226. resultIndices = indices;
  227. }, (kind, data, size, byteOffset, byteStride, normalized) => {
  228. resultAttributes.push({
  229. kind,
  230. data,
  231. size,
  232. byteOffset,
  233. byteStride,
  234. normalized,
  235. });
  236. });
  237. return { indices: resultIndices, attributes: resultAttributes, totalVertices: numPoints };
  238. });
  239. }
  240. throw new Error("Draco decoder module is not available");
  241. }
  242. /**
  243. * Decode Draco compressed mesh data to Babylon geometry.
  244. * @param name The name to use when creating the geometry
  245. * @param scene The scene to use when creating the geometry
  246. * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
  247. * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
  248. * @returns A promise that resolves with the decoded geometry
  249. */
  250. async decodeMeshToGeometryAsync(name, scene, data, attributes) {
  251. const meshData = await this.decodeMeshToMeshDataAsync(data, attributes);
  252. const geometry = new Geometry(name, scene);
  253. if (meshData.indices) {
  254. geometry.setIndices(meshData.indices);
  255. }
  256. for (const attribute of meshData.attributes) {
  257. geometry.setVerticesBuffer(new VertexBuffer(scene.getEngine(), attribute.data, attribute.kind, false, undefined, attribute.byteStride, undefined, attribute.byteOffset, attribute.size, undefined, attribute.normalized, true), meshData.totalVertices);
  258. }
  259. return geometry;
  260. }
  261. /** @internal */
  262. async _decodeMeshToGeometryForGltfAsync(name, scene, data, attributes, gltfNormalizedOverride) {
  263. const meshData = await this.decodeMeshToMeshDataAsync(data, attributes, gltfNormalizedOverride);
  264. const geometry = new Geometry(name, scene);
  265. if (meshData.indices) {
  266. geometry.setIndices(meshData.indices);
  267. }
  268. for (const attribute of meshData.attributes) {
  269. geometry.setVerticesBuffer(new VertexBuffer(scene.getEngine(), attribute.data, attribute.kind, false, undefined, attribute.byteStride, undefined, attribute.byteOffset, attribute.size, undefined, attribute.normalized, true), meshData.totalVertices);
  270. }
  271. return geometry;
  272. }
  273. /**
  274. * Decode Draco compressed mesh data to Babylon vertex data.
  275. * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
  276. * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
  277. * @returns A promise that resolves with the decoded vertex data
  278. * @deprecated Use {@link decodeMeshToGeometryAsync} for better performance in some cases
  279. */
  280. async decodeMeshAsync(data, attributes) {
  281. const meshData = await this.decodeMeshToMeshDataAsync(data, attributes);
  282. const vertexData = new VertexData();
  283. if (meshData.indices) {
  284. vertexData.indices = meshData.indices;
  285. }
  286. for (const attribute of meshData.attributes) {
  287. const floatData = VertexBuffer.GetFloatData(attribute.data, attribute.size, VertexBuffer.GetDataType(attribute.data), attribute.byteOffset, attribute.byteStride, attribute.normalized, meshData.totalVertices);
  288. vertexData.set(floatData, attribute.kind);
  289. }
  290. return vertexData;
  291. }
  292. }
  293. /**
  294. * The configuration. Defaults to the following urls:
  295. * - wasmUrl: "https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js"
  296. * - wasmBinaryUrl: "https://cdn.babylonjs.com/draco_decoder_gltf.wasm"
  297. * - fallbackUrl: "https://cdn.babylonjs.com/draco_decoder_gltf.js"
  298. */
  299. DracoCompression.Configuration = {
  300. decoder: {
  301. wasmUrl: `${Tools._DefaultCdnUrl}/draco_wasm_wrapper_gltf.js`,
  302. wasmBinaryUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.wasm`,
  303. fallbackUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.js`,
  304. },
  305. };
  306. /**
  307. * Default number of workers to create when creating the draco compression object.
  308. */
  309. DracoCompression.DefaultNumWorkers = DracoCompression.GetDefaultNumWorkers();
  310. DracoCompression._Default = null;
  311. //# sourceMappingURL=dracoCompression.js.map