소스 검색

Merge pull request #6227 from bghgary/draco-fix

Better web worker support
David Catuhe 6 년 전
부모
커밋
ab9ae1f665
1개의 변경된 파일279개의 추가작업 그리고 245개의 파일을 삭제
  1. 279 245
      src/Meshes/Compression/dracoCompression.ts

+ 279 - 245
src/Meshes/Compression/dracoCompression.ts

@@ -7,7 +7,188 @@ import { VertexData } from "../../Meshes/mesh.vertexData";
 declare var DracoDecoderModule: any;
 declare var WebAssembly: any;
 
+// WorkerGlobalScope
 declare function importScripts(...urls: string[]): void;
+declare function postMessage(message: any, transfer?: any[]): void;
+
+function loadScriptAsync(url: string): Promise<void> {
+    if (typeof importScripts === "function") {
+        importScripts(url);
+        return Promise.resolve();
+    }
+    else {
+        return new Promise((resolve, reject) => {
+            Tools.LoadScript(url, () => {
+                resolve();
+            }, (message) => {
+                reject(new Error(message));
+            });
+        });
+    }
+}
+
+function loadFileAsync(url: string): Promise<ArrayBuffer> {
+    return new Promise((resolve, reject) => {
+        Tools.LoadFile(url, (data) => {
+            resolve(data as ArrayBuffer);
+        }, undefined, undefined, true, (request, exception) => {
+            reject(exception);
+        });
+    });
+}
+
+function createDecoderAsync(wasmUrl?: string, wasmBinary?: ArrayBuffer, fallbackUrl?: string): Promise<any> | undefined {
+    const decoderUrl = (wasmBinary && wasmUrl) || fallbackUrl;
+    if (decoderUrl) {
+        return loadScriptAsync(decoderUrl).then(() => {
+            return new Promise((resolve) => {
+                DracoDecoderModule({ wasmBinary: wasmBinary }).then((module: any) => {
+                    resolve({ module: module });
+                });
+            });
+        });
+    }
+
+    return undefined;
+}
+
+function decodeMesh(decoderModule: any, dataView: ArrayBufferView, attributes: { [kind: string]: number } | undefined, onIndicesData: (data: Uint32Array) => void, onAttributeData: (kind: string, data: Float32Array) => void): void {
+    const buffer = new decoderModule.DecoderBuffer();
+    buffer.Init(dataView, dataView.byteLength);
+
+    const decoder = new decoderModule.Decoder();
+    let geometry: any;
+    let status: any;
+
+    try {
+        const type = decoder.GetEncodedGeometryType(buffer);
+        switch (type) {
+            case decoderModule.TRIANGULAR_MESH:
+                geometry = new decoderModule.Mesh();
+                status = decoder.DecodeBufferToMesh(buffer, geometry);
+                break;
+            case decoderModule.POINT_CLOUD:
+                geometry = new decoderModule.PointCloud();
+                status = decoder.DecodeBufferToPointCloud(buffer, geometry);
+                break;
+            default:
+                throw new Error(`Invalid geometry type ${type}`);
+        }
+
+        if (!status.ok() || !geometry.ptr) {
+            throw new Error(status.error_msg());
+        }
+
+        const numPoints = geometry.num_points();
+
+        if (type === decoderModule.TRIANGULAR_MESH) {
+            const numFaces = geometry.num_faces();
+            const faceIndices = new decoderModule.DracoInt32Array();
+            try {
+                const indices = new Uint32Array(numFaces * 3);
+                for (let i = 0; i < numFaces; i++) {
+                    decoder.GetFaceFromMesh(geometry, i, faceIndices);
+                    const offset = i * 3;
+                    indices[offset + 0] = faceIndices.GetValue(0);
+                    indices[offset + 1] = faceIndices.GetValue(1);
+                    indices[offset + 2] = faceIndices.GetValue(2);
+                }
+                onIndicesData(indices);
+            }
+            finally {
+                decoderModule.destroy(faceIndices);
+            }
+        }
+
+        const processAttribute = (kind: string, attribute: any) => {
+            const dracoData = new decoderModule.DracoFloat32Array();
+            try {
+                decoder.GetAttributeFloatForAllPoints(geometry, attribute, dracoData);
+                const babylonData = new Float32Array(numPoints * attribute.num_components());
+                for (let i = 0; i < babylonData.length; i++) {
+                    babylonData[i] = dracoData.GetValue(i);
+                }
+                onAttributeData(kind, babylonData);
+            }
+            finally {
+                decoderModule.destroy(dracoData);
+            }
+        };
+
+        if (attributes) {
+            for (const kind in attributes) {
+                const id = attributes[kind];
+                const attribute = decoder.GetAttributeByUniqueId(geometry, id);
+                processAttribute(kind, attribute);
+            }
+        }
+        else {
+            const nativeAttributeTypes: { [kind: string]: string } = {
+                "position": "POSITION",
+                "normal": "NORMAL",
+                "color": "COLOR",
+                "uv": "TEX_COORD"
+            };
+
+            for (const kind in nativeAttributeTypes) {
+                const id = decoder.GetAttributeId(geometry, decoderModule[nativeAttributeTypes[kind]]);
+                if (id !== -1) {
+                    const attribute = decoder.GetAttribute(geometry, id);
+                    processAttribute(kind, attribute);
+                }
+            }
+        }
+    }
+    finally {
+        if (geometry) {
+            decoderModule.destroy(geometry);
+        }
+
+        decoderModule.destroy(decoder);
+        decoderModule.destroy(buffer);
+    }
+}
+
+/**
+ * The worker function that gets converted to a blob url to pass into a worker.
+ */
+function worker(): void {
+    let decoderPromise: Promise<any> | undefined;
+
+    onmessage = (event) => {
+        const data = event.data;
+        switch (data.id) {
+            case "init": {
+                const decoder = data.decoder;
+                decoderPromise = createDecoderAsync(decoder.wasmUrl, decoder.wasmBinary, decoder.fallbackUrl);
+                postMessage("done");
+                break;
+            }
+            case "decodeMesh": {
+                if (!decoderPromise) {
+                    throw new Error("Draco decoder module is not available");
+                }
+                decoderPromise.then((decoder) => {
+                    decodeMesh(decoder.module, data.dataView, data.attributes, (indices) => {
+                        postMessage({ id: "indices", value: indices }, [indices.buffer]);
+                    }, (kind, data) => {
+                        postMessage({ id: kind, value: data }, [data.buffer]);
+                    });
+                    postMessage("done");
+                });
+                break;
+            }
+        }
+    };
+}
+
+function getAbsoluteUrl<T>(url: T): T | string {
+    if (typeof document !== "object" || typeof url !== "string") {
+        return url;
+    }
+
+    return Tools.GetAbsoluteUrl(url);
+}
 
 /**
  * Configuration for Draco compression
@@ -16,7 +197,7 @@ export interface IDracoCompressionConfiguration {
     /**
      * Configuration for the decoder.
      */
-    decoder?: {
+    decoder: {
         /**
          * The url to the WebAssembly module.
          */
@@ -60,20 +241,18 @@ export interface IDracoCompressionConfiguration {
  *
  * 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.
  * Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
- * Use `DracoCompression.DecoderAvailable` to determine if the decoder is available for the current session.
+ * Use `DracoCompression.DecoderAvailable` to determine if the decoder configuration is available for the current context.
  *
- * To decode Draco compressed data, create a DracoCompression object and call decodeMeshAsync:
+ * To decode Draco compressed data, get the default DracoCompression object and call decodeMeshAsync:
  * ```javascript
- *     var dracoCompression = new DracoCompression();
- *     var vertexData = await dracoCompression.decodeMeshAsync(data, {
- *         [VertexBuffer.PositionKind]: 0
- *     });
+ *     var vertexData = await DracoCompression.Default.decodeMeshAsync(data);
  * ```
  *
  * @see https://www.babylonjs-playground.com/#N3EK4B#0
  */
 export class DracoCompression implements IDisposable {
-    private _workerPoolPromise: Promise<WorkerPool>;
+    private _workerPoolPromise?: Promise<WorkerPool>;
+    private _decoderModulePromise?: Promise<any>;
 
     /**
      * The configuration. Defaults to the following urls:
@@ -90,25 +269,11 @@ export class DracoCompression implements IDisposable {
     };
 
     /**
-     * Returns true if the decoder is available.
+     * Returns true if the decoder configuration is available.
      */
     public static get DecoderAvailable(): boolean {
-        if (typeof DracoDecoderModule !== "undefined") {
-            return true;
-        }
-
         const decoder = DracoCompression.Configuration.decoder;
-        if (decoder) {
-            if (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") {
-                return true;
-            }
-
-            if (decoder.fallbackUrl) {
-                return true;
-            }
-        }
-
-        return false;
+        return !!((decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") || decoder.fallbackUrl);
     }
 
     /**
@@ -117,7 +282,7 @@ export class DracoCompression implements IDisposable {
     public static DefaultNumWorkers = DracoCompression.GetDefaultNumWorkers();
 
     private static GetDefaultNumWorkers(): number {
-        if (typeof navigator === "undefined" || !navigator.hardwareConcurrency) {
+        if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
             return 1;
         }
 
@@ -140,24 +305,23 @@ export class DracoCompression implements IDisposable {
 
     /**
      * Constructor
-     * @param numWorkers The number of workers for async operations
+     * @param numWorkers The number of workers for async operations. Specify `0` to disable web workers and run synchronously in the current context.
      */
     constructor(numWorkers = DracoCompression.DefaultNumWorkers) {
-        if (!URL || !URL.createObjectURL) {
-            throw new Error("Object URLs are not available");
-        }
-
-        if (!Worker) {
-            throw new Error("Workers are not available");
-        }
+        const decoder = DracoCompression.Configuration.decoder;
 
-        this._workerPoolPromise = this._loadDecoderWasmBinaryAsync().then((decoderWasmBinary) => {
-            const workerBlobUrl = URL.createObjectURL(new Blob([`(${DracoCompression._Worker.toString()})()`], { type: "application/javascript" }));
-            const workerPromises = new Array<Promise<Worker>>(numWorkers);
-            for (let i = 0; i < workerPromises.length; i++) {
-                workerPromises[i] = new Promise((resolve, reject) => {
-                    const decoder = DracoCompression.Configuration.decoder;
-                    if (decoder) {
+        const decoderWasmBinaryPromise: Promise<ArrayBuffer | undefined> =
+            (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object")
+                ? loadFileAsync(getAbsoluteUrl(decoder.wasmBinaryUrl))
+                : Promise.resolve(undefined);
+
+        if (numWorkers && typeof Worker === "function") {
+            this._workerPoolPromise = decoderWasmBinaryPromise.then((decoderWasmBinary) => {
+                const workerContent = `${loadScriptAsync}${createDecoderAsync}${decodeMesh}(${worker})()`;
+                const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
+                const workerPromises = new Array<Promise<Worker>>(numWorkers);
+                for (let i = 0; i < workerPromises.length; i++) {
+                    workerPromises[i] = new Promise((resolve, reject) => {
                         const worker = new Worker(workerBlobUrl);
                         const onError = (error: ErrorEvent) => {
                             worker.removeEventListener("error", onError);
@@ -177,30 +341,40 @@ export class DracoCompression implements IDisposable {
                         worker.addEventListener("message", onMessage);
 
                         worker.postMessage({
-                            id: "initDecoder",
-                            decoderWasmUrl: decoder.wasmUrl ? Tools.GetAbsoluteUrl(decoder.wasmUrl) : null,
-                            decoderWasmBinary: decoderWasmBinary,
-                            fallbackUrl: decoder.fallbackUrl ? Tools.GetAbsoluteUrl(decoder.fallbackUrl) : null
+                            id: "init",
+                            decoder: {
+                                wasmUrl: getAbsoluteUrl(decoder.wasmUrl),
+                                wasmBinary: decoderWasmBinary,
+                                fallbackUrl: getAbsoluteUrl(decoder.fallbackUrl)
+                            }
                         });
-                    }
-                });
-            }
+                    });
+                }
 
-            return Promise.all(workerPromises).then((workers) => {
-                return new WorkerPool(workers);
+                return Promise.all(workerPromises).then((workers) => {
+                    return new WorkerPool(workers);
+                });
             });
-        });
+        }
+        else {
+            this._decoderModulePromise = decoderWasmBinaryPromise.then((decoderWasmBinary) => {
+                return createDecoderAsync(decoder.wasmUrl, decoderWasmBinary, decoder.fallbackUrl);
+            });
+        }
     }
 
     /**
      * Stop all async operations and release resources.
      */
     public dispose(): void {
-        this._workerPoolPromise.then((workerPool) => {
-            workerPool.dispose();
-        });
+        if (this._workerPoolPromise) {
+            this._workerPoolPromise.then((workerPool) => {
+                workerPool.dispose();
+            });
+        }
 
         delete this._workerPoolPromise;
+        delete this._decoderModulePromise;
     }
 
     /**
@@ -208,218 +382,78 @@ export class DracoCompression implements IDisposable {
      * @returns a promise that resolves when ready
      */
     public whenReadyAsync(): Promise<void> {
-        return this._workerPoolPromise.then(() => { });
+        if (this._workerPoolPromise) {
+            return this._workerPoolPromise.then(() => { });
+        }
+
+        if (this._decoderModulePromise) {
+            return this._decoderModulePromise.then(() => { });
+        }
+
+        return Promise.resolve();
     }
 
-   /**
-     * Decode Draco compressed mesh data to vertex data.
-     * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
-     * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
-     * @returns A promise that resolves with the decoded vertex data
-     */
+    /**
+      * Decode Draco compressed mesh data to vertex data.
+      * @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
+      * @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
+      * @returns A promise that resolves with the decoded vertex data
+      */
     public decodeMeshAsync(data: ArrayBuffer | ArrayBufferView, attributes?: { [kind: string]: number }): Promise<VertexData> {
         const dataView = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
 
-        return this._workerPoolPromise.then((workerPool) => {
-            return new Promise<VertexData>((resolve, reject) => {
-                workerPool.push((worker, onComplete) => {
-                    const vertexData = new VertexData();
-
-                    const onError = (error: ErrorEvent) => {
-                        worker.removeEventListener("error", onError);
-                        worker.removeEventListener("message", onMessage);
-                        reject(error);
-                        onComplete();
-                    };
+        if (this._workerPoolPromise) {
+            return this._workerPoolPromise.then((workerPool) => {
+                return new Promise<VertexData>((resolve, reject) => {
+                    workerPool.push((worker, onComplete) => {
+                        const vertexData = new VertexData();
 
-                    const onMessage = (message: MessageEvent) => {
-                        if (message.data === "done") {
+                        const onError = (error: ErrorEvent) => {
                             worker.removeEventListener("error", onError);
                             worker.removeEventListener("message", onMessage);
-                            resolve(vertexData);
+                            reject(error);
                             onComplete();
-                        }
-                        else if (message.data.id === "indices") {
-                            vertexData.indices = message.data.value;
-                        }
-                        else {
-                            vertexData.set(message.data.value, message.data.id);
-                        }
-                    };
-
-                    worker.addEventListener("error", onError);
-                    worker.addEventListener("message", onMessage);
-
-                    const dataViewCopy = new Uint8Array(dataView.byteLength);
-                    dataViewCopy.set(new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength));
-
-                    worker.postMessage({ id: "decodeMesh", dataView: dataViewCopy, attributes: attributes }, [dataViewCopy.buffer]);
-                });
-            });
-        });
-    }
-
-    /**
-     * The worker function that gets converted to a blob url to pass into a worker.
-     */
-    private static _Worker(): void {
-        const nativeAttributeTypes: { [kind: string]: string } = {
-            "position": "POSITION",
-            "normal": "NORMAL",
-            "color": "COLOR",
-            "uv": "TEX_COORD"
-        };
-
-        // self is actually a DedicatedWorkerGlobalScope
-        const _self = self as any as {
-            onmessage: (event: MessageEvent) => void;
-            postMessage: (message: any, transfer?: any[]) => void;
-            close: () => void;
-        };
-
-        let decoderModulePromise: Promise<any>;
-
-        function initDecoder(decoderWasmUrl: string | undefined, decoderWasmBinary: ArrayBuffer | undefined, fallbackUrl: string | undefined): void {
-            if (decoderWasmUrl && decoderWasmBinary && typeof WebAssembly === "object") {
-                importScripts(decoderWasmUrl);
-                decoderModulePromise = DracoDecoderModule({
-                    wasmBinary: decoderWasmBinary
-                });
-            }
-            else if (fallbackUrl) {
-                importScripts(fallbackUrl);
-                decoderModulePromise = DracoDecoderModule();
-            }
-            else {
-                throw Error("Failed to initialize Draco decoder");
-            }
-
-            _self.postMessage("done");
-        }
+                        };
 
-        function decodeMesh(dataView: ArrayBufferView, attributes: { [kind: string]: number }): void {
-            decoderModulePromise.then((decoderModule) => {
-                const buffer = new decoderModule.DecoderBuffer();
-                buffer.Init(dataView, dataView.byteLength);
-
-                const decoder = new decoderModule.Decoder();
-                let geometry: any;
-                let status: any;
-
-                try {
-                    const type = decoder.GetEncodedGeometryType(buffer);
-                    switch (type) {
-                        case decoderModule.TRIANGULAR_MESH:
-                            geometry = new decoderModule.Mesh();
-                            status = decoder.DecodeBufferToMesh(buffer, geometry);
-                            break;
-                        case decoderModule.POINT_CLOUD:
-                            geometry = new decoderModule.PointCloud();
-                            status = decoder.DecodeBufferToPointCloud(buffer, geometry);
-                            break;
-                        default:
-                            throw new Error(`Invalid geometry type ${type}`);
-                    }
-
-                    if (!status.ok() || !geometry.ptr) {
-                        throw new Error(status.error_msg());
-                    }
-
-                    const numPoints = geometry.num_points();
-
-                    if (type === decoderModule.TRIANGULAR_MESH) {
-                        const numFaces = geometry.num_faces();
-                        const faceIndices = new decoderModule.DracoInt32Array();
-                        try {
-                            const indices = new Uint32Array(numFaces * 3);
-                            for (let i = 0; i < numFaces; i++) {
-                                decoder.GetFaceFromMesh(geometry, i, faceIndices);
-                                const offset = i * 3;
-                                indices[offset + 0] = faceIndices.GetValue(0);
-                                indices[offset + 1] = faceIndices.GetValue(1);
-                                indices[offset + 2] = faceIndices.GetValue(2);
+                        const onMessage = (message: MessageEvent) => {
+                            if (message.data === "done") {
+                                worker.removeEventListener("error", onError);
+                                worker.removeEventListener("message", onMessage);
+                                resolve(vertexData);
+                                onComplete();
                             }
-                            _self.postMessage({ id: "indices", value: indices }, [indices.buffer]);
-                        }
-                        finally {
-                            decoderModule.destroy(faceIndices);
-                        }
-                    }
-
-                    const processAttribute = (kind: string, attribute: any) => {
-                        const dracoData = new decoderModule.DracoFloat32Array();
-                        try {
-                            decoder.GetAttributeFloatForAllPoints(geometry, attribute, dracoData);
-                            const babylonData = new Float32Array(numPoints * attribute.num_components());
-                            for (let i = 0; i < babylonData.length; i++) {
-                                babylonData[i] = dracoData.GetValue(i);
+                            else if (message.data.id === "indices") {
+                                vertexData.indices = message.data.value;
                             }
-                            _self.postMessage({ id: kind, value: babylonData }, [babylonData.buffer]);
-                        }
-                        finally {
-                            decoderModule.destroy(dracoData);
-                        }
-                    };
-
-                    if (attributes) {
-                        for (const kind in attributes) {
-                            const id = attributes[kind];
-                            const attribute = decoder.GetAttributeByUniqueId(geometry, id);
-                            processAttribute(kind, attribute);
-                        }
-                    }
-                    else {
-                        for (const kind in nativeAttributeTypes) {
-                            const id = decoder.GetAttributeId(geometry, decoderModule[nativeAttributeTypes[kind]]);
-                            if (id !== -1) {
-                                const attribute = decoder.GetAttribute(geometry, id);
-                                processAttribute(kind, attribute);
+                            else {
+                                vertexData.set(message.data.value, message.data.id);
                             }
-                        }
-                    }
-                }
-                finally {
-                    if (geometry) {
-                        decoderModule.destroy(geometry);
-                    }
+                        };
 
-                    decoderModule.destroy(decoder);
-                    decoderModule.destroy(buffer);
-                }
+                        worker.addEventListener("error", onError);
+                        worker.addEventListener("message", onMessage);
 
-                _self.postMessage("done");
+                        const dataViewCopy = new Uint8Array(dataView.byteLength);
+                        dataViewCopy.set(new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength));
+
+                        worker.postMessage({ id: "decodeMesh", dataView: dataViewCopy, attributes: attributes }, [dataViewCopy.buffer]);
+                    });
+                });
             });
         }
 
-        _self.onmessage = (event) => {
-            const data = event.data;
-            switch (data.id) {
-                case "initDecoder": {
-                    initDecoder(data.decoderWasmUrl, data.decoderWasmBinary, data.fallbackUrl);
-                    break;
-                }
-                case "decodeMesh": {
-                    decodeMesh(data.dataView, data.attributes);
-                    break;
-                }
-            }
-        };
-    }
-
-    private _loadDecoderWasmBinaryAsync(): Promise<Nullable<ArrayBuffer>> {
-        const decoder = DracoCompression.Configuration.decoder;
-        if (decoder && decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") {
-            const wasmBinaryUrl = Tools.GetAbsoluteUrl(decoder.wasmBinaryUrl);
-            return new Promise((resolve, reject) => {
-                Tools.LoadFile(wasmBinaryUrl, (data) => {
-                    resolve(data as ArrayBuffer);
-                }, undefined, undefined, true, (request, exception) => {
-                    reject(exception);
+        if (this._decoderModulePromise) {
+            return this._decoderModulePromise.then((decoder) => {
+                const vertexData = new VertexData();
+                decodeMesh(decoder.module, dataView, attributes, (indices) => {
+                    vertexData.indices = indices;
+                }, (kind, data) => {
+                    vertexData.set(data, kind);
                 });
+                return vertexData;
             });
         }
-        else {
-            return Promise.resolve(null);
-        }
+
+        throw new Error("Draco decoder module is not available");
     }
 }