dracoCompression.ts 18 KB

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