Browse Source

Use web workers for glTF validation

Gary Hsu 5 years ago
parent
commit
85cde6e3ce

+ 0 - 1
Playground/debug.html

@@ -52,7 +52,6 @@
     <script src="https://preview.babylonjs.com/recast.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.max.js"></script>
     <script src="https://preview.babylonjs.com/gui/babylon.gui.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

+ 0 - 1
Playground/frame.html

@@ -29,7 +29,6 @@
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
     <script src="https://preview.babylonjs.com/earcut.min.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
     <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

+ 0 - 1
Playground/full.html

@@ -17,7 +17,6 @@
     <script src="https://preview.babylonjs.com/recast.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
     <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

+ 3 - 1
Playground/index-local.html

@@ -33,7 +33,6 @@
     <script src="../dist/preview%20release/recast.js"></script>
     <script src="../dist/preview%20release/cannon.js"></script>
     <script src="../dist/preview%20release/Oimo.js"></script>
-    <script src="../dist/preview%20release/gltf_validator.js"></script>
     <script src="../dist/preview%20release/earcut.min.js"></script>
     <!-- Monaco -->
 
@@ -409,6 +408,9 @@
                     wasmBinaryUrl: "../dist/preview%20release/draco_decoder_gltf.wasm",
                     fallbackUrl: "../dist/preview%20release/draco_decoder_gltf.js"
                 };
+                BABYLON.GLTFValidation.Configuration = {
+                    url: "../dist/preview%20release/gltf_validator.js"
+                };
             });
     </script>
 </body>

+ 0 - 1
Playground/index.html

@@ -27,7 +27,6 @@
     <script src="https://preview.babylonjs.com/recast.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/earcut.min.js"></script>
     <!-- Babylon.js -->
     <script src="https://preview.babylonjs.com/babylon.js"></script>

+ 0 - 1
Playground/indexWebGPU.html

@@ -27,7 +27,6 @@
     <script src="https://preview.babylonjs.com/recast.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/earcut.min.js"></script>
     <!-- Babylon.js -->
     <script src="https://preview.babylonjs.com/glslang/glslang.js"></script>

+ 0 - 1
Playground/zipContent/index.html

@@ -11,7 +11,6 @@
         <script src="https://preview.babylonjs.com/ammo.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
         <script src="https://preview.babylonjs.com/babylon.js"></script>
         <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

+ 0 - 1
Viewer/tests/validation/validate.html

@@ -6,7 +6,6 @@
 	<script src="https://preview.babylonjs.com/ammo.js"></script>
 	<script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
 

+ 4 - 1
Viewer/tests/validation/validation.js

@@ -286,13 +286,16 @@ function init() {
     BABYLON.SceneLoader.ShowLoadingScreen = false;
     BABYLON.SceneLoader.ForceFullSceneLoadingForIncremental = true;
 
-    // Draco configuration
     BABYLON.DracoCompression.Configuration.decoder = {
         wasmUrl: "../../dist/preview%20release/draco_wasm_wrapper_gltf.js",
         wasmBinaryUrl: "../../dist/preview%20release/draco_decoder_gltf.wasm",
         fallbackUrl: "../../dist/preview%20release/draco_decoder_gltf.js"
     };
 
+    BABYLON.GLTFValidation.Configuration = {
+        url: "../../dist/preview%20release/gltf_validator.js"
+    };
+
     viewerElement = document.createElement("babylon");
     document.body.appendChild(viewerElement);
 

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/gltf_validator.js


+ 3 - 3
inspector/src/components/actionTabs/actionTabsComponent.tsx

@@ -42,9 +42,9 @@ export class ActionTabsComponent extends React.Component<IActionTabsComponentPro
             : props.initialTab
 
         if (this.props.globalState) {
-            const validationResutls = this.props.globalState.validationResults;
-            if (validationResutls) {
-                if (validationResutls.issues.numErrors || validationResutls.issues.numWarnings) {
+            const validationResults = this.props.globalState.validationResults;
+            if (validationResults) {
+                if (validationResults.issues.numErrors || validationResults.issues.numWarnings) {
                     initialIndex = DebugLayerTab.Tools;
                 }
             }

+ 39 - 14
inspector/src/components/actionTabs/tabs/tools/gltfComponent.tsx

@@ -9,6 +9,9 @@ import { MessageLineComponent } from "../../lines/messageLineComponent";
 import { faCheck, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
 import { TextLineComponent } from "../../lines/textLineComponent";
 import { GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "babylonjs-loaders/glTF/index";
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { IGLTFValidationResults } from "babylonjs-gltf2interface";
 
 interface IGLTFComponentProps {
     scene: Scene;
@@ -16,6 +19,8 @@ interface IGLTFComponentProps {
 }
 
 export class GLTFComponent extends React.Component<IGLTFComponentProps> {
+    private _onValidationResultsUpdatedObserver: Nullable<Observer<Nullable<IGLTFValidationResults>>> = null;
+
     constructor(props: IGLTFComponentProps) {
         super(props);
 
@@ -72,8 +77,28 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps> {
         return `${singularForm}`;
     }
 
+    componentDidMount() {
+        if (this.props.globalState) {
+            this._onValidationResultsUpdatedObserver = this.props.globalState.onValidationResultsUpdatedObservable.add(() => {
+                this.forceUpdate();
+            });
+        }
+    }
+
+    componentWillUnmount() {
+        if (this.props.globalState) {
+            if (this._onValidationResultsUpdatedObserver) {
+                this.props.globalState.onValidationResultsUpdatedObservable.remove(this._onValidationResultsUpdatedObserver);
+            }
+        }
+    }
+
     renderValidation() {
         const validationResults = this.props.globalState.validationResults;
+        if (!validationResults) {
+            return null;
+        }
+
         const issues = validationResults.issues;
 
         return (
@@ -92,7 +117,7 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps> {
                 <TextLineComponent label="Hints" value={issues.numHints.toString()} />
                 <TextLineComponent label="More details" value="Click here" onLink={() => this.openValidationDetails()} />
             </LineContainerComponent>
-        )
+        );
     }
 
     render() {
@@ -125,20 +150,20 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps> {
                     <MessageLineComponent text="You need to reload your file to see these changes" />
                 </LineContainerComponent>
                 <LineContainerComponent globalState={this.props.globalState} title="GLTF EXTENSIONS" closed={true}>
-                    <CheckBoxLineComponent label="MSFT_lod" isSelected={() => extensionStates["MSFT_lod"].enabled} onSelect={value => extensionStates["MSFT_lod"].enabled = value} />
+                    <CheckBoxLineComponent label="MSFT_lod" isSelected={() => extensionStates["MSFT_lod"].enabled} onSelect={(value) => extensionStates["MSFT_lod"].enabled = value} />
                     <FloatLineComponent label="Maximum LODs" target={extensionStates["MSFT_lod"]} propertyName="maxLODsToLoad" additionalClass="gltf-extension-property" isInteger={true} />
-                    <CheckBoxLineComponent label="MSFT_minecraftMesh" isSelected={() => extensionStates["MSFT_minecraftMesh"].enabled} onSelect={value => extensionStates["MSFT_minecraftMesh"].enabled = value} />
-                    <CheckBoxLineComponent label="MSFT_sRGBFactors" isSelected={() => extensionStates["MSFT_sRGBFactors"].enabled} onSelect={value => extensionStates["MSFT_sRGBFactors"].enabled = value} />
-                    <CheckBoxLineComponent label="MSFT_audio_emitter" isSelected={() => extensionStates["MSFT_audio_emitter"].enabled} onSelect={value => extensionStates["MSFT_audio_emitter"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_draco_mesh_compression" isSelected={() => extensionStates["KHR_draco_mesh_compression"].enabled} onSelect={value => extensionStates["KHR_draco_mesh_compression"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_materials_pbrSpecularGlossiness" isSelected={() => extensionStates["KHR_materials_pbrSpecularGlossiness"].enabled} onSelect={value => extensionStates["KHR_materials_pbrSpecularGlossiness"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_materials_clearcoat" isSelected={() => extensionStates["KHR_materials_clearcoat"].enabled} onSelect={value => extensionStates["KHR_materials_clearcoat"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_materials_sheen" isSelected={() => extensionStates["KHR_materials_sheen"].enabled} onSelect={value => extensionStates["KHR_materials_sheen"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_materials_specular" isSelected={() => extensionStates["KHR_materials_specular"].enabled} onSelect={value => extensionStates["KHR_materials_specular"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_materials_unlit" isSelected={() => extensionStates["KHR_materials_unlit"].enabled} onSelect={value => extensionStates["KHR_materials_unlit"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_lights_punctual" isSelected={() => extensionStates["KHR_lights_punctual"].enabled} onSelect={value => extensionStates["KHR_lights_punctual"].enabled = value} />
-                    <CheckBoxLineComponent label="KHR_texture_transform" isSelected={() => extensionStates["KHR_texture_transform"].enabled} onSelect={value => extensionStates["KHR_texture_transform"].enabled = value} />
-                    <CheckBoxLineComponent label="EXT_lights_image_based" isSelected={() => extensionStates["EXT_lights_image_based"].enabled} onSelect={value => extensionStates["EXT_lights_image_based"].enabled = value} />
+                    <CheckBoxLineComponent label="MSFT_minecraftMesh" isSelected={() => extensionStates["MSFT_minecraftMesh"].enabled} onSelect={(value) => extensionStates["MSFT_minecraftMesh"].enabled = value} />
+                    <CheckBoxLineComponent label="MSFT_sRGBFactors" isSelected={() => extensionStates["MSFT_sRGBFactors"].enabled} onSelect={(value) => extensionStates["MSFT_sRGBFactors"].enabled = value} />
+                    <CheckBoxLineComponent label="MSFT_audio_emitter" isSelected={() => extensionStates["MSFT_audio_emitter"].enabled} onSelect={(value) => extensionStates["MSFT_audio_emitter"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_draco_mesh_compression" isSelected={() => extensionStates["KHR_draco_mesh_compression"].enabled} onSelect={(value) => extensionStates["KHR_draco_mesh_compression"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_materials_pbrSpecularGlossiness" isSelected={() => extensionStates["KHR_materials_pbrSpecularGlossiness"].enabled} onSelect={(value) => extensionStates["KHR_materials_pbrSpecularGlossiness"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_materials_clearcoat" isSelected={() => extensionStates["KHR_materials_clearcoat"].enabled} onSelect={(value) => extensionStates["KHR_materials_clearcoat"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_materials_sheen" isSelected={() => extensionStates["KHR_materials_sheen"].enabled} onSelect={(value) => extensionStates["KHR_materials_sheen"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_materials_specular" isSelected={() => extensionStates["KHR_materials_specular"].enabled} onSelect={(value) => extensionStates["KHR_materials_specular"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_materials_unlit" isSelected={() => extensionStates["KHR_materials_unlit"].enabled} onSelect={(value) => extensionStates["KHR_materials_unlit"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_lights_punctual" isSelected={() => extensionStates["KHR_lights_punctual"].enabled} onSelect={(value) => extensionStates["KHR_lights_punctual"].enabled = value} />
+                    <CheckBoxLineComponent label="KHR_texture_transform" isSelected={() => extensionStates["KHR_texture_transform"].enabled} onSelect={(value) => extensionStates["KHR_texture_transform"].enabled = value} />
+                    <CheckBoxLineComponent label="EXT_lights_image_based" isSelected={() => extensionStates["EXT_lights_image_based"].enabled} onSelect={(value) => extensionStates["EXT_lights_image_based"].enabled = value} />
                     <MessageLineComponent text="You need to reload your file to see these changes" />
                 </LineContainerComponent>
                 {

+ 7 - 3
inspector/src/components/globalState.ts

@@ -20,8 +20,8 @@ export class GlobalState {
 
     public sceneImportDefaults: { [key: string]: any } = {};
 
-    public validationResults: IGLTFValidationResults;
-    public onValidationResultsUpdatedObservable = new Observable<IGLTFValidationResults>();
+    public validationResults: Nullable<IGLTFValidationResults> = null;
+    public onValidationResultsUpdatedObservable = new Observable<Nullable<IGLTFValidationResults>>();
 
     public onExtensionLoadedObservable: Observable<IGLTFLoaderExtension>;
     public glTFLoaderExtensionDefaults: { [name: string]: { [key: string]: any } } = {};
@@ -81,7 +81,6 @@ export class GlobalState {
         }
 
         loader.onExtensionLoadedObservable.add((extension: IGLTFLoaderExtension) => {
-
             var extensionState = this.glTFLoaderExtensionDefaults[extension.name];
             if (extensionState !== undefined) {
                 for (const key in extensionState) {
@@ -90,6 +89,11 @@ export class GlobalState {
             }
         });
 
+        if (this.validationResults) {
+            this.validationResults = null;
+            this.onValidationResultsUpdatedObservable.notifyObservers(null);
+        }
+
         loader.onValidatedObservable.add((results: IGLTFValidationResults) => {
             this.validationResults = results;
             this.onValidationResultsUpdatedObservable.notifyObservers(results);

+ 11 - 30
loaders/src/glTF/glTFFileLoader.ts

@@ -16,11 +16,7 @@ import { WebRequest } from "babylonjs/Misc/webRequest";
 import { IFileRequest } from "babylonjs/Misc/fileRequest";
 import { Logger } from 'babylonjs/Misc/logger';
 import { DataReader, IDataBuffer } from 'babylonjs/Misc/dataReader';
-
-/**
- * glTF validator object
- */
-declare var GLTFValidator: GLTF2.IGLTFValidator;
+import { GLTFValidation } from './glTFValidation';
 
 /**
  * Mode that determines the coordinate system to use.
@@ -522,7 +518,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
         }
 
         return scene._requestFile(url, (data, response) => {
-            this._validateAsync(scene, data, Tools.GetFolderPath(url), Tools.GetFilename(url));
+            this._validate(scene, data, Tools.GetFolderPath(url), Tools.GetFilename(url));
             onSuccess({ json: this._parseJson(data as string) }, response);
         }, onProgress, true, false, onError);
     }
@@ -530,7 +526,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
     /** @hidden */
     public readFile(scene: Scene, file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: any) => void): IFileRequest {
         return scene._readFile(file, (data) => {
-            this._validateAsync(scene, data, "file:", file.name);
+            this._validate(scene, data, "file:", file.name);
             if (useArrayBuffer) {
                 const arrayBuffer = data as ArrayBuffer;
                 this._unpackBinaryAsync(new DataReader({
@@ -608,7 +604,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
 
     /** @hidden */
     public directLoad(scene: Scene, data: string): any {
-        this._validateAsync(scene, data);
+        this._validate(scene, data);
         return { json: this._parseJson(data) };
     }
 
@@ -647,36 +643,21 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
         });
     }
 
-    private _validateAsync(scene: Scene, data: string | ArrayBuffer, rootUrl = "", fileName?: string): Promise<void> {
-        if (!this.validate || typeof GLTFValidator === "undefined") {
-            return Promise.resolve();
+    private _validate(scene: Scene, data: string | ArrayBuffer, rootUrl = "", fileName = ""): void {
+        if (!this.validate) {
+            return;
         }
 
         this._startPerformanceCounter("Validate JSON");
-
-        const options: GLTF2.IGLTFValidationOptions = {
-            externalResourceFunction: (uri) => {
-                return this.preprocessUrlAsync(rootUrl + uri)
-                    .then((url) => scene._loadFileAsync(url, undefined, true, true))
-                    .then((data) => new Uint8Array(data as ArrayBuffer));
-            }
-        };
-
-        if (fileName) {
-            options.uri = (rootUrl === "file:" ? fileName : rootUrl + fileName);
-        }
-
-        const promise = (data instanceof ArrayBuffer)
-            ? GLTFValidator.validateBytes(new Uint8Array(data), options)
-            : GLTFValidator.validateString(data, options);
-
-        return promise.then((result) => {
+        GLTFValidation.ValidateAsync(data, rootUrl, fileName, (uri) => {
+            return this.preprocessUrlAsync(rootUrl + uri).then((url) => (scene._loadFileAsync(url, undefined, true, true) as Promise<ArrayBuffer>));
+        }).then((result) => {
             this._endPerformanceCounter("Validate JSON");
             this.onValidatedObservable.notifyObservers(result);
             this.onValidatedObservable.clear();
         }, (reason) => {
             this._endPerformanceCounter("Validate JSON");
-            Tools.Warn(`Failed to validate: ${reason}`);
+            Tools.Warn(`Failed to validate: ${reason.message}`);
             this.onValidatedObservable.clear();
         });
     }

+ 140 - 0
loaders/src/glTF/glTFValidation.ts

@@ -0,0 +1,140 @@
+import * as GLTF2 from 'babylonjs-gltf2interface';
+import { Tools } from 'babylonjs/Misc/tools';
+
+declare var GLTFValidator: GLTF2.IGLTFValidator;
+
+// WorkerGlobalScope
+declare function importScripts(...urls: string[]): void;
+declare function postMessage(message: any, transfer?: any[]): void;
+
+function validateAsync(data: string | ArrayBuffer, rootUrl: string, fileName: string, getExternalResource: (uri: string) => Promise<ArrayBuffer>): Promise<GLTF2.IGLTFValidationResults> {
+    const options: GLTF2.IGLTFValidationOptions = {
+        externalResourceFunction: (uri) => getExternalResource(uri).then((value) => new Uint8Array(value))
+    };
+
+    if (fileName) {
+        options.uri = (rootUrl === "file:" ? fileName : rootUrl + fileName);
+    }
+
+    return (data instanceof ArrayBuffer)
+        ? GLTFValidator.validateBytes(new Uint8Array(data), options)
+        : GLTFValidator.validateString(data, options);
+}
+
+/**
+ * The worker function that gets converted to a blob url to pass into a worker.
+ */
+function workerFunc(): void {
+    const pendingExternalResources: Array<{ resolve: (data: any) => void, reject: (reason: any) => void }> = [];
+
+    onmessage = (message) => {
+        const data = message.data;
+        switch (data.id) {
+            case "init": {
+                importScripts(data.url);
+                break;
+            }
+            case "validate": {
+                validateAsync(data.data, data.rootUrl, data.fileName, (uri) => new Promise((resolve, reject) => {
+                    const index = pendingExternalResources.length;
+                    pendingExternalResources.push({ resolve, reject });
+                    postMessage({ id: "getExternalResource", index: index, uri: uri });
+                })).then((value) => {
+                    postMessage({ id: "validate.resolve", value: value });
+                }, (reason) => {
+                    postMessage({ id: "validate.reject", reason: reason });
+                });
+                break;
+            }
+            case "getExternalResource.resolve": {
+                pendingExternalResources[data.index].resolve(data.value);
+                break;
+            }
+            case "getExternalResource.reject": {
+                pendingExternalResources[data.index].reject(data.reason);
+                break;
+            }
+        }
+    };
+}
+
+/**
+ * Configuration for glTF validation
+ */
+export interface IGLTFValidationConfiguration {
+    /**
+     * The url of the glTF validator.
+     */
+    url: string;
+}
+
+/**
+ * glTF validation
+ */
+export class GLTFValidation {
+    /**
+     * The configuration. Defaults to `{ url: "https://preview.babylonjs.com/gltf_validator.js" }`.
+     */
+    public static Configuration: IGLTFValidationConfiguration = {
+        url: "https://preview.babylonjs.com/gltf_validator.js"
+    };
+
+    private static _LoadScriptPromise: Promise<void>;
+
+    public static ValidateAsync(data: string | ArrayBuffer, rootUrl: string, fileName: string, getExternalResource: (uri: string) => Promise<ArrayBuffer>): Promise<GLTF2.IGLTFValidationResults>
+    {
+        if (typeof Worker === "function") {
+            return new Promise((resolve, reject) => {
+                const workerContent = `${validateAsync}(${workerFunc})()`;
+                const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
+                const worker = new Worker(workerBlobUrl);
+
+                const onError = (error: ErrorEvent) => {
+                    worker.removeEventListener("error", onError);
+                    worker.removeEventListener("message", onMessage);
+                    reject(error);
+                };
+
+                const onMessage = (message: MessageEvent) => {
+                    const data = message.data;
+                    switch (data.id) {
+                        case "getExternalResource": {
+                            getExternalResource(data.uri).then((value) => {
+                                worker.postMessage({ id: "getExternalResource.resolve", index: data.index, value: value }, [value]);
+                            }, (reason) => {
+                                worker.postMessage({ id: "getExternalResource.reject", index: data.index, reason: reason });
+                            });
+                            break;
+                        }
+                        case "validate.resolve": {
+                            worker.removeEventListener("error", onError);
+                            worker.removeEventListener("message", onMessage);
+                            resolve(data.value);
+                            break;
+                        }
+                        case "validate.reject": {
+                            worker.removeEventListener("error", onError);
+                            worker.removeEventListener("message", onMessage);
+                            reject(data.reason);
+                        }
+                    }
+                };
+
+                worker.addEventListener("error", onError);
+                worker.addEventListener("message", onMessage);
+
+                worker.postMessage({ id: "init", url: Tools.GetAbsoluteUrl(this.Configuration.url) });
+                worker.postMessage({ id: "validate", data: data, rootUrl: rootUrl, fileName: fileName });
+            });
+        }
+        else {
+            if (!this._LoadScriptPromise) {
+                this._LoadScriptPromise = Tools.LoadScriptAsync(this.Configuration.url);
+            }
+
+            return this._LoadScriptPromise.then(() => {
+                return validateAsync(data, rootUrl, fileName, getExternalResource);
+            });
+        }
+    }
+}

+ 1 - 0
loaders/src/glTF/index.ts

@@ -1,4 +1,5 @@
 export * from "./glTFFileLoader";
+export * from "./glTFValidation";
 import * as GLTF1 from "./1.0";
 import * as GLTF2 from "./2.0";
 export {

+ 6 - 1
loaders/src/legacy/legacy-glTF.ts

@@ -1,4 +1,5 @@
 import * as FileLoader from "../glTF/glTFFileLoader";
+import * as Validation from "../glTF/glTFValidation";
 
 /**
  * This is the entry point for the UMD module.
@@ -10,6 +11,10 @@ if (typeof globalObject !== "undefined") {
     for (var key in FileLoader) {
         (<any>globalObject).BABYLON[key] = (<any>FileLoader)[key];
     }
+    for (var key in Validation) {
+        (<any>globalObject).BABYLON[key] = (<any>Validation)[key];
+    }
 }
 
-export * from "../glTF/glTFFileLoader";
+export * from "../glTF/glTFFileLoader";
+export * from "../glTF/glTFValidation";

+ 3 - 1
localDev/index-views.html

@@ -10,7 +10,6 @@
     <script src="../dist/preview%20release/Oimo.js"></script>
     <script src="../dist/preview%20release/ammo.js"></script>
     <script src="../dist/preview%20release/recast.js"></script>
-    <script src="../dist/preview%20release/gltf_validator.js"></script>
     <script src="../Tools/DevLoader/BabylonLoader.js"></script>
 
     <style>
@@ -116,6 +115,9 @@
                     wasmBinaryUrl: "../dist/preview%20release/draco_decoder_gltf.wasm",
                     fallbackUrl: "../dist/preview%20release/draco_decoder_gltf.js"
                 };
+                BABYLON.GLTFValidation.Configuration = {
+                    url: "../dist/preview%20release/gltf_validator.js"
+                };
 
                 if (BABYLON.Engine.isSupported()) {
                     if (typeof createEngine !== "undefined") {

+ 3 - 1
localDev/index.html

@@ -10,7 +10,6 @@
     <script src="../dist/preview%20release/Oimo.js"></script>
     <script src="../dist/preview%20release/ammo.js"></script>
     <script src="../dist/preview%20release/recast.js"></script>
-    <script src="../dist/preview%20release/gltf_validator.js"></script>
     <script src="../Tools/DevLoader/BabylonLoader.js"></script>
 
     <style>
@@ -84,6 +83,9 @@
                     wasmBinaryUrl: "../dist/preview%20release/draco_decoder_gltf.wasm",
                     fallbackUrl: "../dist/preview%20release/draco_decoder_gltf.js"
                 };
+                BABYLON.GLTFValidation.Configuration = {
+                    url: "../dist/preview%20release/gltf_validator.js"
+                };
 
                 if (BABYLON.Engine.isSupported()) {
                     if (typeof createEngine !== "undefined") {

+ 0 - 1
sandbox/debug.html

@@ -36,7 +36,6 @@
     <script src="https://preview.babylonjs.com/ammo.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.max.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
 

+ 3 - 1
sandbox/index-local.html

@@ -10,7 +10,6 @@
     <script src="../dist/preview%20release/ammo.js"></script>
     <script src="../dist/preview%20release/cannon.js"></script>
     <script src="../dist/preview%20release/Oimo.js"></script>
-    <script src="../dist/preview%20release/gltf_validator.js"></script>
     <script src="../Tools/DevLoader/BabylonLoader.js"></script>
 </head>
 
@@ -83,6 +82,9 @@
                     wasmBinaryUrl: "../dist/preview%20release/draco_decoder_gltf.wasm",
                     fallbackUrl: "../dist/preview%20release/draco_decoder_gltf.js"
                 };
+                BABYLON.GLTFValidation.Configuration = {
+                    url: "../dist/preview%20release/gltf_validator.js"
+                };
             });
     </script>
 </body>

+ 0 - 1
sandbox/index.html

@@ -18,7 +18,6 @@
     <script src="https://preview.babylonjs.com/ammo.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/gltf_validator.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
 

+ 27 - 48
src/Meshes/Compression/dracoCompression.ts

@@ -11,47 +11,14 @@ declare var WebAssembly: any;
 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(wasmBinary?: ArrayBuffer): Promise<any> {
+    return new Promise((resolve) => {
+        DracoDecoderModule({ wasmBinary: wasmBinary }).then((module: any) => {
+            resolve({ module: module });
         });
     });
 }
 
-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);
@@ -160,7 +127,10 @@ function worker(): void {
         switch (data.id) {
             case "init": {
                 const decoder = data.decoder;
-                decoderPromise = createDecoderAsync(decoder.wasmUrl, decoder.wasmBinary, decoder.fallbackUrl);
+                if (decoder.url) {
+                    importScripts(decoder.url);
+                    decoderPromise = createDecoderAsync(decoder.wasmBinary);
+                }
                 postMessage("done");
                 break;
             }
@@ -310,14 +280,18 @@ export class DracoCompression implements IDisposable {
     constructor(numWorkers = DracoCompression.DefaultNumWorkers) {
         const decoder = DracoCompression.Configuration.decoder;
 
-        const decoderWasmBinaryPromise: Promise<ArrayBuffer | undefined> =
-            (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object")
-                ? loadFileAsync(getAbsoluteUrl(decoder.wasmBinaryUrl))
-                : Promise.resolve(undefined);
+        const decoderInfo: { url: string | undefined, wasmBinaryPromise: Promise<ArrayBuffer | undefined> } =
+            (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") ? {
+                url: decoder.wasmUrl,
+                wasmBinaryPromise: Tools.LoadFileAsync(getAbsoluteUrl(decoder.wasmBinaryUrl))
+            } : {
+                url: decoder.fallbackUrl,
+                wasmBinaryPromise: Promise.resolve(undefined)
+            };
 
         if (numWorkers && typeof Worker === "function") {
-            this._workerPoolPromise = decoderWasmBinaryPromise.then((decoderWasmBinary) => {
-                const workerContent = `${loadScriptAsync}${createDecoderAsync}${decodeMesh}(${worker})()`;
+            this._workerPoolPromise = decoderInfo.wasmBinaryPromise.then((decoderWasmBinary) => {
+                const workerContent = `${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++) {
@@ -343,9 +317,8 @@ export class DracoCompression implements IDisposable {
                         worker.postMessage({
                             id: "init",
                             decoder: {
-                                wasmUrl: getAbsoluteUrl(decoder.wasmUrl),
+                                url: getAbsoluteUrl(decoderInfo.url),
                                 wasmBinary: decoderWasmBinary,
-                                fallbackUrl: getAbsoluteUrl(decoder.fallbackUrl)
                             }
                         });
                     });
@@ -357,8 +330,14 @@ export class DracoCompression implements IDisposable {
             });
         }
         else {
-            this._decoderModulePromise = decoderWasmBinaryPromise.then((decoderWasmBinary) => {
-                return createDecoderAsync(decoder.wasmUrl, decoderWasmBinary, decoder.fallbackUrl);
+            this._decoderModulePromise = decoderInfo.wasmBinaryPromise.then((decoderWasmBinary) => {
+                if (!decoderInfo.url) {
+                    throw new Error("Draco decoder module is not available");
+                }
+
+                return Tools.LoadScriptAsync(decoderInfo.url).then(() => {
+                    return createDecoderAsync(decoderWasmBinary);
+                });
             });
         }
     }

+ 7 - 23
src/Misc/tools.ts

@@ -404,29 +404,13 @@ export class Tools {
      * @param scriptId defines the id of the script element
      * @returns a promise request object
      */
-    public static LoadScriptAsync(scriptUrl: string, scriptId?: string): Promise<boolean> {
-        return new Promise<boolean>((resolve, reject) => {
-            if (!DomManagement.IsWindowObjectExist()) {
-                resolve(false);
-                return;
-            }
-            var head = document.getElementsByTagName('head')[0];
-            var script = document.createElement('script');
-            script.setAttribute('type', 'text/javascript');
-            script.setAttribute('src', scriptUrl);
-            if (scriptId) {
-                script.id = scriptId;
-            }
-
-            script.onload = () => {
-                resolve(true);
-            };
-
-            script.onerror = (e) => {
-                resolve(false);
-            };
-
-            head.appendChild(script);
+    public static LoadScriptAsync(scriptUrl: string, scriptId?: string): Promise<void> {
+        return new Promise((resolve, reject) => {
+            this.LoadScript(scriptUrl, () => {
+                resolve();
+            }, (message, exception) => {
+                reject(exception);
+            });
         });
     }
 

+ 0 - 1
tests/validation/validate.html

@@ -9,7 +9,6 @@
 	<script src="https://preview.babylonjs.com/ammo.js"></script>
 	<script src="https://preview.babylonjs.com/cannon.js"></script>
 	<script src="https://preview.babylonjs.com/Oimo.js"></script>
-	<script src="https://preview.babylonjs.com/gltf_validator.js"></script>
 	<script src="https://preview.babylonjs.com/babylon.js"></script>
 	<script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
 

+ 4 - 1
tests/validation/validation.js

@@ -374,13 +374,16 @@ function init() {
     BABYLON.SceneLoader.ShowLoadingScreen = false;
     BABYLON.SceneLoader.ForceFullSceneLoadingForIncremental = true;
 
-    // Draco configuration
     BABYLON.DracoCompression.Configuration.decoder = {
         wasmUrl: "../../dist/preview%20release/draco_wasm_wrapper_gltf.js",
         wasmBinaryUrl: "../../dist/preview%20release/draco_decoder_gltf.wasm",
         fallbackUrl: "../../dist/preview%20release/draco_decoder_gltf.js"
     };
 
+    BABYLON.GLTFValidation.Configuration = {
+        url: "../../dist/preview%20release/gltf_validator.js"
+    };
+
     canvas = document.createElement("canvas");
     canvas.className = "renderCanvas";
     document.body.appendChild(canvas);