dracoCompression.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import { Tools } from "../../Misc/tools";
  2. import { WorkerPool } from '../../Misc/workerPool';
  3. import { Nullable } from "../../types";
  4. import { IDisposable } from "../../scene";
  5. import { VertexData } from "../../Meshes/mesh.vertexData";
  6. declare var DracoDecoderModule: any;
  7. declare var WebAssembly: any;
  8. declare function importScripts(...urls: string[]): void;
  9. /**
  10. * Configuration for Draco compression
  11. */
  12. export interface IDracoCompressionConfiguration {
  13. /**
  14. * Configuration for the decoder.
  15. */
  16. decoder?: {
  17. /**
  18. * The url to the WebAssembly module.
  19. */
  20. wasmUrl?: string;
  21. /**
  22. * The url to the WebAssembly binary.
  23. */
  24. wasmBinaryUrl?: string;
  25. /**
  26. * The url to the fallback JavaScript module.
  27. */
  28. fallbackUrl?: string;
  29. };
  30. }
  31. /**
  32. * Draco compression (https://google.github.io/draco/)
  33. *
  34. * This class wraps the Draco module.
  35. *
  36. * **Encoder**
  37. *
  38. * The encoder is not currently implemented.
  39. *
  40. * **Decoder**
  41. *
  42. * 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.
  43. *
  44. * To update the configuration, use the following code:
  45. * ```javascript
  46. * DracoCompression.Configuration = {
  47. * decoder: {
  48. * wasmUrl: "<url to the WebAssembly library>",
  49. * wasmBinaryUrl: "<url to the WebAssembly binary>",
  50. * fallbackUrl: "<url to the fallback JavaScript library>",
  51. * }
  52. * };
  53. * ```
  54. *
  55. * Draco has two versions, one for WebAssembly and one for JavaScript. The decoder configuration can be set to only support Webssembly or only support the JavaScript version.
  56. * Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
  57. * Use `DracoCompression.DecoderAvailable` to determine if the decoder is available for the current session.
  58. *
  59. * To decode Draco compressed data, create a DracoCompression object and call decodeMeshAsync:
  60. * ```javascript
  61. * var dracoCompression = new DracoCompression();
  62. * var vertexData = await dracoCompression.decodeMeshAsync(data, {
  63. * [VertexBuffer.PositionKind]: 0
  64. * });
  65. * ```
  66. *
  67. * @see https://www.babylonjs-playground.com/#N3EK4B#0
  68. */
  69. export class DracoCompression implements IDisposable {
  70. private _workerPoolPromise: Promise<WorkerPool>;
  71. /**
  72. * The configuration. Defaults to the following urls:
  73. * - wasmUrl: "https://preview.babylonjs.com/draco_wasm_wrapper_gltf.js"
  74. * - wasmBinaryUrl: "https://preview.babylonjs.com/draco_decoder_gltf.wasm"
  75. * - fallbackUrl: "https://preview.babylonjs.com/draco_decoder_gltf.js"
  76. */
  77. public static Configuration: IDracoCompressionConfiguration = {
  78. decoder: {
  79. wasmUrl: "https://preview.babylonjs.com/draco_wasm_wrapper_gltf.js",
  80. wasmBinaryUrl: "https://preview.babylonjs.com/draco_decoder_gltf.wasm",
  81. fallbackUrl: "https://preview.babylonjs.com/draco_decoder_gltf.js"
  82. }
  83. };
  84. /**
  85. * Returns true if the decoder is available.
  86. */
  87. public static get DecoderAvailable(): boolean {
  88. if (typeof DracoDecoderModule !== "undefined") {
  89. return true;
  90. }
  91. const decoder = DracoCompression.Configuration.decoder;
  92. if (decoder) {
  93. if (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") {
  94. return true;
  95. }
  96. if (decoder.fallbackUrl) {
  97. return true;
  98. }
  99. }
  100. return false;
  101. }
  102. /**
  103. * Default number of workers to create when creating the draco compression object.
  104. */
  105. public static DefaultNumWorkers = DracoCompression.GetDefaultNumWorkers();
  106. private static GetDefaultNumWorkers(): number {
  107. const hardwareConcurrency = navigator && navigator.hardwareConcurrency;
  108. if (!hardwareConcurrency) {
  109. return 1;
  110. }
  111. // Use 50% of the available logical processors but capped at 4.
  112. return Math.min(Math.floor(hardwareConcurrency * 0.5), 4);
  113. }
  114. /**
  115. * Constructor
  116. * @param numWorkers The number of workers for async operations
  117. */
  118. constructor(numWorkers = DracoCompression.DefaultNumWorkers) {
  119. if (!URL || !URL.createObjectURL) {
  120. throw new Error("Object URLs are not available");
  121. }
  122. if (!Worker) {
  123. throw new Error("Workers are not available");
  124. }
  125. this._workerPoolPromise = this._loadDecoderWasmBinaryAsync().then((decoderWasmBinary) => {
  126. const workerBlobUrl = URL.createObjectURL(new Blob([`(${DracoCompression._Worker.toString()})()`], { type: "application/javascript" }));
  127. const workerPromises = new Array<Promise<Worker>>(numWorkers);
  128. for (let i = 0; i < workerPromises.length; i++) {
  129. workerPromises[i] = new Promise((resolve, reject) => {
  130. const decoder = DracoCompression.Configuration.decoder;
  131. if (decoder) {
  132. const worker = new Worker(workerBlobUrl);
  133. const onError = (error: ErrorEvent) => {
  134. worker.removeEventListener("error", onError);
  135. worker.removeEventListener("message", onMessage);
  136. reject(error);
  137. };
  138. const onMessage = (message: MessageEvent) => {
  139. if (message.data === "done") {
  140. worker.removeEventListener("error", onError);
  141. worker.removeEventListener("message", onMessage);
  142. resolve(worker);
  143. }
  144. };
  145. worker.addEventListener("error", onError);
  146. worker.addEventListener("message", onMessage);
  147. worker.postMessage({
  148. id: "initDecoder",
  149. decoderWasmUrl: decoder.wasmUrl ? Tools.GetAbsoluteUrl(decoder.wasmUrl) : null,
  150. decoderWasmBinary: decoderWasmBinary,
  151. fallbackUrl: decoder.fallbackUrl ? Tools.GetAbsoluteUrl(decoder.fallbackUrl) : null
  152. });
  153. }
  154. });
  155. }
  156. return Promise.all(workerPromises).then((workers) => {
  157. return new WorkerPool(workers);
  158. });
  159. });
  160. }
  161. /**
  162. * Stop all async operations and release resources.
  163. */
  164. public dispose(): void {
  165. this._workerPoolPromise.then((workerPool) => {
  166. workerPool.dispose();
  167. });
  168. delete this._workerPoolPromise;
  169. }
  170. /**
  171. * Returns a promise that resolves when ready. Call this manually to ensure draco compression is ready before use.
  172. * @returns a promise that resolves when ready
  173. */
  174. public whenReadyAsync(): Promise<void> {
  175. return this._workerPoolPromise.then(() => { });
  176. }
  177. /**
  178. * Decode Draco compressed mesh data to vertex data.
  179. * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
  180. * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
  181. * @returns A promise that resolves with the decoded vertex data
  182. */
  183. public decodeMeshAsync(data: ArrayBuffer | ArrayBufferView, attributes?: { [kind: string]: number }): Promise<VertexData> {
  184. const dataView = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
  185. return this._workerPoolPromise.then((workerPool) => {
  186. return new Promise<VertexData>((resolve, reject) => {
  187. workerPool.push((worker, onComplete) => {
  188. const vertexData = new VertexData();
  189. const onError = (error: ErrorEvent) => {
  190. worker.removeEventListener("error", onError);
  191. worker.removeEventListener("message", onMessage);
  192. reject(error);
  193. onComplete();
  194. };
  195. const onMessage = (message: MessageEvent) => {
  196. if (message.data === "done") {
  197. worker.removeEventListener("error", onError);
  198. worker.removeEventListener("message", onMessage);
  199. resolve(vertexData);
  200. onComplete();
  201. }
  202. else if (message.data.id === "indices") {
  203. vertexData.indices = message.data.value;
  204. }
  205. else {
  206. vertexData.set(message.data.value, message.data.id);
  207. }
  208. };
  209. worker.addEventListener("error", onError);
  210. worker.addEventListener("message", onMessage);
  211. const dataViewCopy = new Uint8Array(dataView.byteLength);
  212. dataViewCopy.set(new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength));
  213. worker.postMessage({ id: "decodeMesh", dataView: dataViewCopy, attributes: attributes }, [dataViewCopy.buffer]);
  214. });
  215. });
  216. });
  217. }
  218. /**
  219. * The worker function that gets converted to a blob url to pass into a worker.
  220. */
  221. private static _Worker(): void {
  222. const nativeAttributeTypes: { [kind: string]: string } = {
  223. "position": "POSITION",
  224. "normal": "NORMAL",
  225. "color": "COLOR",
  226. "uv": "TEX_COORD"
  227. };
  228. // self is actually a DedicatedWorkerGlobalScope
  229. const _self = self as any as {
  230. onmessage: (event: MessageEvent) => void;
  231. postMessage: (message: any, transfer?: any[]) => void;
  232. close: () => void;
  233. };
  234. let decoderModulePromise: Promise<any>;
  235. function initDecoder(decoderWasmUrl: string | undefined, decoderWasmBinary: ArrayBuffer | undefined, fallbackUrl: string | undefined): void {
  236. if (decoderWasmUrl && decoderWasmBinary && typeof WebAssembly === "object") {
  237. importScripts(decoderWasmUrl);
  238. decoderModulePromise = DracoDecoderModule({
  239. wasmBinary: decoderWasmBinary
  240. });
  241. }
  242. else if (fallbackUrl) {
  243. importScripts(fallbackUrl);
  244. decoderModulePromise = DracoDecoderModule();
  245. }
  246. else {
  247. throw Error("Failed to initialize Draco decoder");
  248. }
  249. _self.postMessage("done");
  250. }
  251. function decodeMesh(dataView: ArrayBufferView, attributes: { [kind: string]: number }): void {
  252. decoderModulePromise.then((decoderModule) => {
  253. const buffer = new decoderModule.DecoderBuffer();
  254. buffer.Init(dataView, dataView.byteLength);
  255. const decoder = new decoderModule.Decoder();
  256. let geometry: any;
  257. let status: any;
  258. try {
  259. const type = decoder.GetEncodedGeometryType(buffer);
  260. switch (type) {
  261. case decoderModule.TRIANGULAR_MESH:
  262. geometry = new decoderModule.Mesh();
  263. status = decoder.DecodeBufferToMesh(buffer, geometry);
  264. break;
  265. case decoderModule.POINT_CLOUD:
  266. geometry = new decoderModule.PointCloud();
  267. status = decoder.DecodeBufferToPointCloud(buffer, geometry);
  268. break;
  269. default:
  270. throw new Error(`Invalid geometry type ${type}`);
  271. }
  272. if (!status.ok() || !geometry.ptr) {
  273. throw new Error(status.error_msg());
  274. }
  275. const numPoints = geometry.num_points();
  276. if (type === decoderModule.TRIANGULAR_MESH) {
  277. const numFaces = geometry.num_faces();
  278. const faceIndices = new decoderModule.DracoInt32Array();
  279. try {
  280. const indices = new Uint32Array(numFaces * 3);
  281. for (let i = 0; i < numFaces; i++) {
  282. decoder.GetFaceFromMesh(geometry, i, faceIndices);
  283. const offset = i * 3;
  284. indices[offset + 0] = faceIndices.GetValue(0);
  285. indices[offset + 1] = faceIndices.GetValue(1);
  286. indices[offset + 2] = faceIndices.GetValue(2);
  287. }
  288. _self.postMessage({ id: "indices", value: indices }, [indices.buffer]);
  289. }
  290. finally {
  291. decoderModule.destroy(faceIndices);
  292. }
  293. }
  294. const processAttribute = (kind: string, attribute: any) => {
  295. const dracoData = new decoderModule.DracoFloat32Array();
  296. try {
  297. decoder.GetAttributeFloatForAllPoints(geometry, attribute, dracoData);
  298. const babylonData = new Float32Array(numPoints * attribute.num_components());
  299. for (let i = 0; i < babylonData.length; i++) {
  300. babylonData[i] = dracoData.GetValue(i);
  301. }
  302. _self.postMessage({ id: kind, value: babylonData }, [babylonData.buffer]);
  303. }
  304. finally {
  305. decoderModule.destroy(dracoData);
  306. }
  307. };
  308. if (attributes) {
  309. for (const kind in attributes) {
  310. const id = attributes[kind];
  311. const attribute = decoder.GetAttributeByUniqueId(geometry, id);
  312. processAttribute(kind, attribute);
  313. }
  314. }
  315. else {
  316. for (const kind in nativeAttributeTypes) {
  317. const id = decoder.GetAttributeId(geometry, decoderModule[nativeAttributeTypes[kind]]);
  318. if (id !== -1) {
  319. const attribute = decoder.GetAttribute(geometry, id);
  320. processAttribute(kind, attribute);
  321. }
  322. }
  323. }
  324. }
  325. finally {
  326. if (geometry) {
  327. decoderModule.destroy(geometry);
  328. }
  329. decoderModule.destroy(decoder);
  330. decoderModule.destroy(buffer);
  331. }
  332. _self.postMessage("done");
  333. });
  334. }
  335. _self.onmessage = (event) => {
  336. const data = event.data;
  337. switch (data.id) {
  338. case "initDecoder": {
  339. initDecoder(data.decoderWasmUrl, data.decoderWasmBinary, data.fallbackUrl);
  340. break;
  341. }
  342. case "decodeMesh": {
  343. decodeMesh(data.dataView, data.attributes);
  344. break;
  345. }
  346. }
  347. };
  348. }
  349. private _loadDecoderWasmBinaryAsync(): Promise<Nullable<ArrayBuffer>> {
  350. const decoder = DracoCompression.Configuration.decoder;
  351. if (decoder && decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") {
  352. const wasmBinaryUrl = Tools.GetAbsoluteUrl(decoder.wasmBinaryUrl);
  353. return new Promise((resolve, reject) => {
  354. Tools.LoadFile(wasmBinaryUrl, (data) => {
  355. resolve(data as ArrayBuffer);
  356. }, undefined, undefined, true, (request, exception) => {
  357. reject(exception);
  358. });
  359. });
  360. }
  361. else {
  362. return Promise.resolve(null);
  363. }
  364. }
  365. }