Kaynağa Gözat

Add support for glTF validation

Gary Hsu 7 yıl önce
ebeveyn
işleme
c85f454e71

+ 1 - 0
Playground/debug.html

@@ -37,6 +37,7 @@
         <!-- Babylon.js -->
         <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>

+ 1 - 0
Playground/frame.html

@@ -31,6 +31,7 @@
         <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>

+ 1 - 0
Playground/full.html

@@ -28,6 +28,7 @@
         <!-- Babylon.js -->
         <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>

+ 1 - 0
Playground/index-local.html

@@ -16,6 +16,7 @@
         <!-- Dependencies -->
         <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 -->
 

+ 1 - 0
Playground/index.html

@@ -36,6 +36,7 @@
         <!-- Dependencies -->
         <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>

+ 3 - 2
Playground/indexStable.html

@@ -35,8 +35,9 @@
         <script src="js/libs/fileSaver.js"></script>
         <!-- Physics -->
         <script src="https://cdn.babylonjs.com/cannon.js"></script>
-        <script src="https://cdn.babylonjs.com/Oimo.js"></script>        
-        <script src="https://preview.babylonjs.com/earcut.min.js"></script>
+        <script src="https://cdn.babylonjs.com/Oimo.js"></script>
+        <script src="https://cdn.babylonjs.com/gltf_validator.js"></script>
+        <script src="https://cdn.babylonjs.com/earcut.min.js"></script>
         <!-- Monaco -->
         <script src="node_modules/monaco-editor/min/vs/loader.js"></script>
         <!-- Babylon.js -->

+ 1 - 0
Playground/zipContent/index.html

@@ -10,6 +10,7 @@
         <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.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>

+ 2 - 2
Tools/Gulp/config.json

@@ -1796,10 +1796,10 @@
                     "../../loaders/src/glTF/2.0/babylon.glTFLoaderInterfaces.ts",
                     "../../loaders/src/glTF/2.0/babylon.glTFLoader.ts",
                     "../../loaders/src/glTF/2.0/babylon.glTFLoaderExtension.ts",
-                    "../../loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_lod.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_minecraftMesh.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_sRGBFactors.ts",
+                    "../../loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_materials_unlit.ts",
@@ -1822,10 +1822,10 @@
                     "../../loaders/src/glTF/2.0/babylon.glTFLoaderInterfaces.ts",
                     "../../loaders/src/glTF/2.0/babylon.glTFLoader.ts",
                     "../../loaders/src/glTF/2.0/babylon.glTFLoaderExtension.ts",
-                    "../../loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_lod.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_minecraftMesh.ts",
                     "../../loaders/src/glTF/2.0/Extensions/MSFT_sRGBFactors.ts",
+                    "../../loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.ts",
                     "../../loaders/src/glTF/2.0/Extensions/KHR_materials_unlit.ts",

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

@@ -3,9 +3,9 @@
 <head>
 	<title>BabylonJS - Build validation page</title>
 	<link href="index.css" rel="stylesheet" />
-    <script src="https://preview.babylonjs.com/draco_decoder.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>
 

+ 49 - 1
dist/preview release/glTF2Interface/babylon.glTF2Interface.d.ts

@@ -6,7 +6,7 @@ declare module "babylonjs-gltf2interface" {
     export = BABYLON.GLTF2;
 }
 /**
- * Moduel for glTF 2.0 Interface
+ * Module for glTF 2.0 Interface
  */
 declare module BABYLON.GLTF2 {
     /**
@@ -853,3 +853,51 @@ declare module BABYLON.GLTF2 {
         textures?: ITexture[];
     }
 }
+
+/**
+ * Interface for glTF validation results
+ */
+interface IGLTFValidationResults {
+    info: {
+        generator: string;
+        hasAnimations: boolean;
+        hasDefaultScene: boolean;
+        hasMaterials: boolean;
+        hasMorphTargets: boolean;
+        hasSkins: boolean;
+        hasTextures: boolean;
+        maxAttributesUsed: number;
+        primitivesCount: number
+    };
+    issues: {
+        messages: Array<string>;
+        numErrors: number;
+        numHints: number;
+        numInfos: number;
+        numWarnings: number;
+        truncated: boolean
+    };
+    mimeType: string;
+    uri: string;
+    validatedAt: string;
+    validatorVersion: string;
+}
+
+/**
+ * Interface for glTF validation options
+ */
+interface IGLTFValidationOptions {
+    uri?: string;
+    externalResourceFunction?: (uri: string) => Promise<Uint8Array>;
+    validateAccessorData?: boolean;
+    maxIssues?: number;
+    ignoredIssues?: Array<string>;
+    severityOverrides?: Object;
+}
+
+/**
+ * glTF validator object
+ */
+declare var GLTFValidator: {
+    validateString: (json: string, options?: IGLTFValidationOptions) => Promise<IGLTFValidationResults>;
+};

Dosya farkı çok büyük olduğundan ihmal edildi
+ 8482 - 0
dist/preview release/gltf_validator.js


+ 9 - 0
inspector/sass/tabs/_gltfTab.scss

@@ -22,6 +22,15 @@
             }
         }
 
+        .gltf-icon {
+            width      : 1em;
+            height     : 1em;
+            line-height: 1em;
+            display    : inline-block;
+            font-family: $font-family-icons;
+            margin-right:10px;
+        }
+
         .gltf-checkbox {
             @extend .gltf-action;
             &:before {

+ 139 - 76
inspector/src/tabs/GLTFTab.ts

@@ -1,4 +1,4 @@
-import { Mesh, NullEngine, PBRMaterial, Scene, SceneLoader, StandardMaterial, Texture, TransformNode } from "babylonjs";
+import { Mesh, Nullable, NullEngine, PBRMaterial, Scene, SceneLoader, StandardMaterial, Texture, TransformNode } from "babylonjs";
 import { GLTF2, GLTFFileLoader } from "babylonjs-loaders";
 import { GLTF2Export } from "babylonjs-serializers";
 import { DetailPanel } from "../details/DetailPanel";
@@ -9,25 +9,17 @@ import { Inspector } from "../Inspector";
 import { Tab } from "./Tab";
 import { TabBar } from "./TabBar";
 
+import "babylonjs-gltf2interface";
 import * as Split from "Split";
 
-interface ILoaderDefaults {
-    [extensionName: string]: {
-        [key: string]: any
-    },
-    extensions: {
-        [extensionName: string]: {
-            [key: string]: any
-        }
-    }
-}
-
 export class GLTFTab extends Tab {
-    private static _LoaderDefaults: ILoaderDefaults | null = null;
+    private static _LoaderDefaults: any = null;
+    private static _ValidationResults: Nullable<IGLTFValidationResults> = null;
+    private static _OnValidationResultsUpdated: Nullable<(results: IGLTFValidationResults) => void> = null;
 
     private _inspector: Inspector;
     private _actions: HTMLDivElement;
-    private _detailsPanel: DetailPanel | null = null;
+    private _detailsPanel: Nullable<DetailPanel> = null;
     private _split: any;
 
     public static get IsSupported(): boolean {
@@ -38,18 +30,14 @@ export class GLTFTab extends Tab {
     public static _Initialize(): void {
         // Must register with OnPluginActivatedObservable as early as possible to override the loader defaults.
         SceneLoader.OnPluginActivatedObservable.add((loader: GLTFFileLoader) => {
-            if (loader.name === "gltf" && GLTFTab._LoaderDefaults) {
-                const defaults = GLTFTab._LoaderDefaults;
-                for (const key in defaults) {
-                    if (key !== "extensions") {
-                        (loader as any)[key] = GLTFTab._LoaderDefaults[key];
-                    }
-                }
+            if (loader.name === "gltf") {
+                GLTFTab._ApplyLoaderDefaults(loader);
+
+                loader.onValidatedObservable.add(results => {
+                    GLTFTab._ValidationResults = results;
 
-                loader.onExtensionLoadedObservable.add(extension => {
-                    const extensionDefaults = defaults.extensions[extension.name];
-                    for (const key in extensionDefaults) {
-                        (extension as any)[key] = extensionDefaults[key];
+                    if (GLTFTab._OnValidationResultsUpdated) {
+                        GLTFTab._OnValidationResultsUpdated(results);
                     }
                 });
             }
@@ -57,12 +45,12 @@ export class GLTFTab extends Tab {
     }
 
     constructor(tabbar: TabBar, inspector: Inspector) {
-        super(tabbar, 'GLTF');
+        super(tabbar, "GLTF");
 
         this._inspector = inspector;
-        this._panel = Helpers.CreateDiv('tab-panel') as HTMLDivElement;
-        this._actions = Helpers.CreateDiv('gltf-actions', this._panel) as HTMLDivElement;
-        this._actions.addEventListener('click', event => {
+        this._panel = Helpers.CreateDiv("tab-panel") as HTMLDivElement;
+        this._actions = Helpers.CreateDiv("gltf-actions", this._panel) as HTMLDivElement;
+        this._actions.addEventListener("click", event => {
             this._closeDetailsPanel();
         });
 
@@ -82,95 +70,170 @@ export class GLTFTab extends Tab {
     }
 
     private _addImport() {
-        const importTitle = Helpers.CreateDiv('gltf-title', this._actions);
-        importTitle.textContent = 'Import';
+        const importTitle = Helpers.CreateDiv("gltf-title", this._actions);
+        importTitle.textContent = "Import";
 
-        const importActions = Helpers.CreateDiv('gltf-actions', this._actions) as HTMLDivElement;
+        const importActions = Helpers.CreateDiv("gltf-actions", this._actions) as HTMLDivElement;
 
-        this._getLoaderDefaultsAsync().then(defaults => {
-            importTitle.addEventListener('click', event => {
+        GLTFTab._GetLoaderDefaultsAsync().then(defaults => {
+            const loaderAction = Helpers.CreateDiv("gltf-action", importActions) as HTMLDivElement;
+            loaderAction.innerText = "Loader";
+            loaderAction.addEventListener("click", event => {
                 this._showLoaderDefaults(defaults);
                 event.stopPropagation();
             });
 
-            importActions.addEventListener('click', event => {
-                this._showLoaderDefaults(defaults);
-                event.stopPropagation();
-            });
-
-            const extensionsTitle = Helpers.CreateDiv('gltf-title', importActions) as HTMLDivElement;
+            const extensionsTitle = Helpers.CreateDiv("gltf-title", importActions) as HTMLDivElement;
             extensionsTitle.textContent = "Extensions";
 
             for (const extensionName in defaults.extensions) {
                 const extensionDefaults = defaults.extensions[extensionName];
 
-                const extensionAction = Helpers.CreateDiv('gltf-action', importActions);
-                extensionAction.addEventListener('click', event => {
+                const extensionAction = Helpers.CreateDiv("gltf-action", importActions);
+                extensionAction.addEventListener("click", event => {
                     if (this._showLoaderExtensionDefaults(extensionDefaults)) {
                         event.stopPropagation();
                     }
                 });
 
-                const checkbox = Helpers.CreateElement('span', 'gltf-checkbox', extensionAction);
+                const checkbox = Helpers.CreateElement("span", "gltf-checkbox", extensionAction);
 
                 if (extensionDefaults.enabled) {
-                    checkbox.classList.add('action', 'active');
+                    checkbox.classList.add("action", "active");
                 }
 
-                checkbox.addEventListener('click', () => {
-                    checkbox.classList.toggle('active');
-                    extensionDefaults.enabled = checkbox.classList.contains('active');
+                checkbox.addEventListener("click", () => {
+                    checkbox.classList.toggle("active");
+                    extensionDefaults.enabled = checkbox.classList.contains("active");
                 });
 
-                const label = Helpers.CreateElement('span', null, extensionAction);
+                const label = Helpers.CreateElement("span", null, extensionAction);
                 label.textContent = extensionName;
             }
+
+            let validationTitle: Nullable<HTMLDivElement> = null;
+            let validationAction: Nullable<HTMLDivElement> = null;
+
+            GLTFTab._OnValidationResultsUpdated = results => {
+                if (!validationTitle) {
+                    validationTitle = Helpers.CreateDiv("gltf-title", importActions) as HTMLDivElement;
+                }
+
+                if (!validationAction) {
+                    validationAction = Helpers.CreateDiv("gltf-action", importActions) as HTMLDivElement;
+                    validationAction.addEventListener("click", event => {
+                        GLTFTab._ShowValidationResults();
+                        event.stopPropagation();
+                    });
+                }
+
+                validationTitle.textContent = results.uri === "null" ? "Validation" : `Validation - ${BABYLON.Tools.GetFilename(results.uri)}`;
+                GLTFTab._FormatValidationResultsShort(validationAction, results);
+            };
+
+            if (GLTFTab._ValidationResults) {
+                GLTFTab._OnValidationResultsUpdated(GLTFTab._ValidationResults);
+            }
         });
     }
 
-    private static _EnumeratePublic(obj: any, callback: (key: string, value: any) => void): void {
+    private static _FormatValidationResultsShort(validationAction: HTMLDivElement, results: IGLTFValidationResults): void {
+        validationAction.innerHTML = "";
+
+        let message = "";
+        const add = (count: number, issueType: string): void => {
+            if (count) {
+                if (message) {
+                    message += ", ";
+                }
+
+                message += count === 1 ? `${count} ${issueType}` : `${count} ${issueType}s`;
+            }
+        };
+
+        const issues = results.issues;
+        add(issues.numErrors, "error");
+        add(issues.numWarnings, "warning");
+        add(issues.numInfos, "info");
+        add(issues.numHints, "hint");
+
+        const actionDiv = Helpers.CreateDiv("gltf-action", validationAction) as HTMLDivElement;
+
+        const iconSpan = Helpers.CreateElement("span", "gltf-icon", actionDiv, issues.numErrors ? "The asset contains errors." : "The asset is valid.");
+        iconSpan.textContent = issues.numErrors ? "\uf057" : "\uf058";
+        iconSpan.style.color = issues.numErrors ? "red" : "green";
+
+        const messageSpan = Helpers.CreateElement("span", "gltf-icon", actionDiv);
+        messageSpan.textContent = message || "No issues";
+    }
+
+    private static _ShowValidationResults(): void {
+        if (GLTFTab._ValidationResults) {
+            const win = window.open("", "_blank");
+            if (win) {
+                // TODO: format this better and use generator registry (https://github.com/KhronosGroup/glTF-Generator-Registry)
+                win.document.title = "glTF Validation Results";
+                win.document.body.innerText = JSON.stringify(GLTFTab._ValidationResults, null, 2);
+                win.document.body.style.whiteSpace = "pre";
+                win.document.body.style.fontFamily = `monospace`;
+                win.document.body.style.fontSize = `14px`;
+                win.focus();
+            }
+        }
+    }
+
+    private static _ApplyLoaderDefaults(loader: GLTFFileLoader): void {
+        const defaults = GLTFTab._LoaderDefaults;
+        if (defaults) {
+            for (const key in defaults) {
+                if (key !== "extensions") {
+                    (loader as any)[key] = defaults[key];
+                }
+            }
+
+            loader.onExtensionLoadedObservable.add(extension => {
+                const extensionDefaults = defaults.extensions[extension.name];
+                for (const key in extensionDefaults) {
+                    (extension as any)[key] = extensionDefaults[key];
+                }
+            });
+        }
+    }
+
+    private static _GetPublic(obj: any): any {
+        const result: any = {};
         for (const key in obj) {
-            if (key !== "name" && key[0] !== '_') {
+            if (key !== "name" && key[0] !== "_") {
                 const value = obj[key];
                 const type = typeof value;
                 if (type !== "object" && type !== "function" && type !== "undefined") {
-                    callback(key, value);
+                    result[key] = value;
                 }
             }
         }
+        return result;
     }
 
-    private _getLoaderDefaultsAsync(): Promise<ILoaderDefaults> {
+    /** @hidden */
+    public static _GetLoaderDefaultsAsync(): Promise<any> {
         if (GLTFTab._LoaderDefaults) {
             return Promise.resolve(GLTFTab._LoaderDefaults);
         }
 
-        const defaults: ILoaderDefaults = {
-            extensions: {}
-        };
-
         const engine = new NullEngine();
         const scene = new Scene(engine);
-
         const loader = new GLTFFileLoader();
-        GLTFTab._EnumeratePublic(loader, (key, value) => {
-            defaults[key] = value;
-        });
 
+        GLTFTab._LoaderDefaults = GLTFTab._GetPublic(loader);
+        GLTFTab._LoaderDefaults.extensions = {};
         loader.onExtensionLoadedObservable.add(extension => {
-            const extensionDefaults: any = {};
-            GLTFTab._EnumeratePublic(extension, (key, value) => {
-                extensionDefaults[key] = value;
-            });
-            defaults.extensions[extension.name] = extensionDefaults;
+            GLTFTab._LoaderDefaults.extensions[extension.name] = GLTFTab._GetPublic(extension);
         });
 
         const data = '{ "asset": { "version": "2.0" } }';
         return loader.importMeshAsync([], scene, data, "").then(() => {
-            scene.dispose();
             engine.dispose();
-
-            return (GLTFTab._LoaderDefaults = defaults);
+            return GLTFTab._LoaderDefaults;
         });
     }
 
@@ -182,7 +245,7 @@ export class GLTFTab extends Tab {
             this._split = Split([this._actions, this._detailsPanel.toHtml()], {
                 blockDrag: this._inspector.popupMode,
                 sizes: [50, 50],
-                direction: 'vertical'
+                direction: "vertical"
             });
         }
 
@@ -232,17 +295,17 @@ export class GLTFTab extends Tab {
     }
 
     private _addExport() {
-        const exportTitle = Helpers.CreateDiv('gltf-title', this._actions);
-        exportTitle.textContent = 'Export';
+        const exportTitle = Helpers.CreateDiv("gltf-title", this._actions);
+        exportTitle.textContent = "Export";
 
-        const exportActions = Helpers.CreateDiv('gltf-actions', this._actions) as HTMLDivElement;
+        const exportActions = Helpers.CreateDiv("gltf-actions", this._actions) as HTMLDivElement;
 
-        const name = Helpers.CreateInput('gltf-input', exportActions);
+        const name = Helpers.CreateInput("gltf-input", exportActions);
         name.placeholder = "File name...";
 
-        const button = Helpers.CreateElement('button', 'gltf-button', exportActions) as HTMLButtonElement;
-        button.innerText = 'Export GLB';
-        button.addEventListener('click', () => {
+        const button = Helpers.CreateElement("button", "gltf-button", exportActions) as HTMLButtonElement;
+        button.innerText = "Export GLB";
+        button.addEventListener("click", () => {
             GLTF2Export.GLBAsync(this._inspector.scene, name.value || "scene", {
                 shouldExportTransformNode: transformNode => !GLTFTab._IsSkyBox(transformNode)
             }).then((glb) => {

+ 3 - 0
inspector/tsconfig.json

@@ -28,6 +28,9 @@
             "babylonjs-gui": [
                 "../../dist/preview release/gui/babylon.gui.module.d.ts"
             ],
+            "babylonjs-gltf2interface": [
+                "../../dist/preview release/glTF2Interface/babylon.glTF2Interface.d.ts"
+            ],
             "babylonjs-loaders": [
                 "../../dist/preview release/loaders/babylonjs.loaders.module.d.ts"
             ],

+ 6 - 0
inspector/webpack.config.js

@@ -39,6 +39,12 @@ module.exports = {
             commonjs2: "babylonjs-gui",
             amd: "babylonjs-gui"
         },
+        "babylonjs-gltf2interface": {
+            root: "BABYLON",
+            commonjs: "babylonjs-gltf2interface",
+            commonjs2: "babylonjs-gltf2interface",
+            amd: "babylonjs-gltf2interface"
+        },
         "babylonjs-loaders": {
             root: "BABYLON",
             commonjs: "babylonjs-loaders",

+ 1 - 1
loaders/src/glTF/2.0/babylon.glTFLoader.ts

@@ -1031,7 +1031,7 @@ module BABYLON.GLTF2 {
             }
 
             return Promise.all(promises).then(() => {
-                babylonAnimationGroup.normalize(this._parent._normalizeAnimationGroupsToBeginAtZero ? 0 : null);
+                babylonAnimationGroup.normalize(0);
                 return babylonAnimationGroup;
             });
         }

+ 0 - 1
loaders/src/glTF/2.0/babylon.glTFLoaderInterfaces.ts

@@ -1,5 +1,4 @@
 /// <reference path="../../../../dist/preview release/babylon.d.ts"/>
-/// <reference path="../../../../dist/preview release/glTF2Interface/babylon.glTF2Interface.d.ts"/>
 
 module BABYLON.GLTF2 {
     /**

+ 124 - 70
loaders/src/glTF/babylon.glTFFileLoader.ts

@@ -1,4 +1,5 @@
 /// <reference path="../../../dist/preview release/babylon.d.ts"/>
+/// <reference path="../../../dist/preview release/glTF2Interface/babylon.glTF2Interface.d.ts"/>
 
 module BABYLON {
     /**
@@ -41,12 +42,12 @@ module BABYLON {
      */
     export interface IGLTFLoaderData {
         /**
-         * JSON that represents the glTF.
+         * Object that represents the glTF JSON.
          */
         json: Object;
 
         /**
-         * The BIN chunk of a binary glTF
+         * The BIN chunk of a binary glTF.
          */
         bin: Nullable<ArrayBufferView>;
     }
@@ -103,7 +104,9 @@ module BABYLON {
         /** @hidden */
         public static _CreateGLTFLoaderV2: (parent: GLTFFileLoader) => IGLTFLoader;
 
-        // #region Common options
+        // --------------
+        // Common options
+        // --------------
 
         /**
          * Raised when the asset has been parsed
@@ -122,9 +125,9 @@ module BABYLON {
             this._onParsedObserver = this.onParsedObservable.add(callback);
         }
 
-        // #endregion
-
-        // #region V1 options
+        // ----------
+        // V1 options
+        // ----------
 
         /**
          * Set this property to false to disable incremental loading which delays the loader from calling the success callback until after loading the meshes and shaders.
@@ -141,9 +144,9 @@ module BABYLON {
          */
         public static HomogeneousCoordinates = false;
 
-        // #endregion
-
-        // #region V2 options
+        // ----------
+        // V2 options
+        // ----------
 
         /**
          * The coordinate system mode. Defaults to AUTO.
@@ -177,9 +180,6 @@ module BABYLON {
          */
         public transparencyAsCoverage = false;
 
-        /** @hidden */
-        public _normalizeAnimationGroupsToBeginAtZero = true;
-
         /**
          * Function called before loading a url referenced by the asset.
          */
@@ -327,28 +327,6 @@ module BABYLON {
         }
 
         /**
-         * Returns a promise that resolves when the asset is completely loaded.
-         * @returns a promise that resolves when the asset is completely loaded.
-         */
-        public whenCompleteAsync(): Promise<void> {
-            return new Promise((resolve, reject) => {
-                this.onCompleteObservable.addOnce(() => {
-                    resolve();
-                });
-                this.onErrorObservable.addOnce(reason => {
-                    reject(reason);
-                });
-            });
-        }
-
-        /**
-         * The loader state or null if the loader is not active.
-         */
-        public get loaderState(): Nullable<GLTFLoaderState> {
-            return this._loader ? this._loader.state : null;
-        }
-
-        /**
          * Defines if the loader logging is enabled.
          */
         public get loggingEnabled(): boolean {
@@ -394,7 +372,27 @@ module BABYLON {
             }
         }
 
-        // #endregion
+        /**
+         * Defines if the loader should validate the asset.
+         */
+        public validate = false;
+
+        /**
+         * Observable raised after validation when validate is set to true. The event data is the result of the validation.
+         */
+        public readonly onValidatedObservable = new Observable<IGLTFValidationResults>();
+
+        private _onValidatedObserver: Nullable<Observer<IGLTFValidationResults>>;
+
+        /**
+         * Callback raised after a loader extension is created.
+         */
+        public set onValidated(callback: (results: IGLTFValidationResults) => void) {
+            if (this._onValidatedObserver) {
+                this.onValidatedObservable.remove(this._onValidatedObserver);
+            }
+            this._onValidatedObserver = this.onValidatedObservable.add(callback);
+        }
 
         private _loader: Nullable<IGLTFLoader> = null;
 
@@ -449,9 +447,8 @@ module BABYLON {
          * @returns a promise containg the loaded meshes, particles, skeletons and animations
          */
         public importMeshAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<{ meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], animationGroups: AnimationGroup[] }> {
-            return Promise.resolve().then(() => {
+            return this._parseAsync(scene, data, rootUrl, fileName).then(loaderData => {
                 this._log(`Loading ${fileName || ""}`);
-                const loaderData = this._parse(data);
                 this._loader = this._getLoader(loaderData);
                 return this._loader.importMeshAsync(meshesNames, scene, loaderData, rootUrl, onProgress, fileName);
             });
@@ -467,9 +464,8 @@ module BABYLON {
          * @returns a promise which completes when objects have been loaded to the scene
          */
         public loadAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<void> {
-            return Promise.resolve().then(() => {
+            return this._parseAsync(scene, data, rootUrl, fileName).then(loaderData => {
                 this._log(`Loading ${fileName || ""}`);
-                const loaderData = this._parse(data);
                 this._loader = this._getLoader(loaderData);
                 return this._loader.loadAsync(scene, loaderData, rootUrl, onProgress, fileName);
             });
@@ -485,9 +481,8 @@ module BABYLON {
          * @returns The loaded asset container
          */
         public loadAssetContainerAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<AssetContainer> {
-            return Promise.resolve().then(() => {
+            return this._parseAsync(scene, data, rootUrl, fileName).then(loaderData => {
                 this._log(`Loading ${fileName || ""}`);
-                const loaderData = this._parse(data);
                 this._loader = this._getLoader(loaderData);
                 return this._loader.importMeshAsync(null, scene, loaderData, rootUrl, onProgress, fileName).then(result => {
                     const container = new AssetContainer(scene);
@@ -523,29 +518,76 @@ module BABYLON {
             return new GLTFFileLoader();
         }
 
-        private _parse(data: string | ArrayBuffer): IGLTFLoaderData {
-            this._startPerformanceCounter("Parse");
+        /**
+         * The loader state or null if the loader is not active.
+         */
+        public get loaderState(): Nullable<GLTFLoaderState> {
+            return this._loader ? this._loader.state : null;
+        }
+
+        /**
+         * Returns a promise that resolves when the asset is completely loaded.
+         * @returns a promise that resolves when the asset is completely loaded.
+         */
+        public whenCompleteAsync(): Promise<void> {
+            return new Promise((resolve, reject) => {
+                this.onCompleteObservable.addOnce(() => {
+                    resolve();
+                });
+                this.onErrorObservable.addOnce(reason => {
+                    reject(reason);
+                });
+            });
+        }
+
+        private _parseAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, fileName?: string): Promise<IGLTFLoaderData> {
+            return Promise.resolve().then(() => {
+                const unpacked = (data instanceof ArrayBuffer) ? this._unpackBinary(data) : { json: data, bin: null };
+
+                return this._validateAsync(scene, unpacked.json, rootUrl, fileName).then(() => {
+                    this._startPerformanceCounter("Parse JSON");
+                    this._log(`JSON length: ${unpacked.json.length}`);
+
+                    const loaderData: IGLTFLoaderData = {
+                        json: JSON.parse(unpacked.json),
+                        bin: unpacked.bin
+                    };
+
+                    this._endPerformanceCounter("Parse JSON");
 
-            let parsedData: IGLTFLoaderData;
-            if (data instanceof ArrayBuffer) {
-                this._log(`Parsing binary`);
-                parsedData = this._parseBinary(data);
+                    this.onParsedObservable.notifyObservers(loaderData);
+                    this.onParsedObservable.clear();
+
+                    return loaderData;
+                });
+            });
+        }
+
+        private _validateAsync(scene: Scene, json: string, rootUrl: string, fileName?: string): Promise<void> {
+            if (!this.validate || typeof GLTFValidator === "undefined") {
+                return Promise.resolve();
             }
-            else {
-                this._log(`Parsing JSON`);
-                this._log(`JSON length: ${data.length}`);
 
-                parsedData = {
-                    json: JSON.parse(data),
-                    bin: null
-                };
+            this._startPerformanceCounter("Validate JSON");
+
+            const options: IGLTFValidationOptions = {
+                externalResourceFunction: uri => {
+                    return this.preprocessUrlAsync(rootUrl + uri)
+                        .then(url => scene._loadFileAsync(url, true, true))
+                        .then(data => new Uint8Array(data as ArrayBuffer));
+                }
+            };
+
+            if (fileName && fileName.substr(0, 5) !== "data:") {
+                options.uri = (rootUrl === "file:" ? fileName : `${rootUrl}${fileName}`);
             }
 
-            this.onParsedObservable.notifyObservers(parsedData);
-            this.onParsedObservable.clear();
+            return GLTFValidator.validateString(json, options).then(result => {
+                this._endPerformanceCounter("Validate JSON");
 
-            this._endPerformanceCounter("Parse");
-            return parsedData;
+                this.onValidatedObservable.notifyObservers(result);
+                this.onValidatedObservable.clear();
+            });
         }
 
         private _getLoader(loaderData: IGLTFLoaderData): IGLTFLoader {
@@ -584,13 +626,14 @@ module BABYLON {
             return createLoader(this);
         }
 
-        private _parseBinary(data: ArrayBuffer): IGLTFLoaderData {
+        private _unpackBinary(data: ArrayBuffer): { json: string, bin: Nullable<ArrayBufferView> } {
+            this._startPerformanceCounter("Unpack binary");
+            this._log(`Binary length: ${data.byteLength}`);
+
             const Binary = {
                 Magic: 0x46546C67
             };
 
-            this._log(`Binary length: ${data.byteLength}`);
-
             const binaryReader = new BinaryReader(data);
 
             const magic = binaryReader.readUint32();
@@ -604,15 +647,26 @@ module BABYLON {
                 this._log(`Binary version: ${version}`);
             }
 
+            let unpacked: { json: string, bin: Nullable<ArrayBufferView> };
             switch (version) {
-                case 1: return this._parseV1(binaryReader);
-                case 2: return this._parseV2(binaryReader);
+                case 1: {
+                    unpacked = this._unpackBinaryV1(binaryReader);
+                    break;
+                }
+                case 2: {
+                    unpacked = this._unpackBinaryV2(binaryReader);
+                    break;
+                }
+                default: {
+                    throw new Error("Unsupported version: " + version);
+                }
             }
 
-            throw new Error("Unsupported version: " + version);
+            this._endPerformanceCounter("Unpack binary");
+            return unpacked;
         }
 
-        private _parseV1(binaryReader: BinaryReader): IGLTFLoaderData {
+        private _unpackBinaryV1(binaryReader: BinaryReader): { json: string, bin: Nullable<ArrayBufferView> } {
             const ContentFormat = {
                 JSON: 0
             };
@@ -625,10 +679,10 @@ module BABYLON {
             const contentLength = binaryReader.readUint32();
             const contentFormat = binaryReader.readUint32();
 
-            let content: Object;
+            let content: string;
             switch (contentFormat) {
                 case ContentFormat.JSON: {
-                    content = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(contentLength)));
+                    content = GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(contentLength));
                     break;
                 }
                 default: {
@@ -645,7 +699,7 @@ module BABYLON {
             };
         }
 
-        private _parseV2(binaryReader: BinaryReader): IGLTFLoaderData {
+        private _unpackBinaryV2(binaryReader: BinaryReader): { json: string, bin: Nullable<ArrayBufferView> } {
             const ChunkFormat = {
                 JSON: 0x4E4F534A,
                 BIN: 0x004E4942
@@ -662,7 +716,7 @@ module BABYLON {
             if (chunkFormat !== ChunkFormat.JSON) {
                 throw new Error("First chunk format is not JSON");
             }
-            const json = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(chunkLength)));
+            const json = GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(chunkLength));
 
             // Look for BIN chunk
             let bin: Nullable<Uint8Array> = null;
@@ -710,7 +764,7 @@ module BABYLON {
             };
         }
 
-        private static _compareVersion(a: { major: number, minor: number }, b: { major: number, minor: number }) {
+        private static _compareVersion(a: { major: number, minor: number }, b: { major: number, minor: number }): number {
             if (a.major > b.major) return 1;
             if (a.major < b.major) return -1;
             if (a.minor > b.minor) return 1;

+ 1 - 0
localDev/index.html

@@ -8,6 +8,7 @@
     <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.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>
     <script src="src/webgl-debug.js"></script>
 

+ 1 - 0
sandbox/index-local.html

@@ -6,6 +6,7 @@
     <link href="index.css" rel="stylesheet" />
     <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>
 <body>

+ 2 - 1
sandbox/index.html

@@ -32,8 +32,9 @@
 
     <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>
+    <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script> 
 
     <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
     <script src="https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>

+ 22 - 1
sandbox/index.js

@@ -71,10 +71,23 @@ if (BABYLON.Engine.isSupported()) {
     // This is really important to tell Babylon.js to use decomposeLerp and matrix interpolation
     BABYLON.Animation.AllowMatricesInterpolation = true;
 
+    // Update the defaults of the GLTFTab in the inspector.
+    INSPECTOR.GLTFTab._GetLoaderDefaultsAsync().then(function (defaults) {
+        defaults.validate = true;
+    });
+
     // Setting up some GLTF values
     BABYLON.GLTFFileLoader.IncrementalLoading = false;
     BABYLON.SceneLoader.OnPluginActivatedObservable.add(function (plugin) {
         currentPluginName = plugin.name;
+        if (currentPluginName === "gltf") {
+            plugin.onValidatedObservable.add(function (results) {
+                if (results.issues.numErrors > 0) {
+                    debugLayerEnabled = true;
+                    debugLayerLastActiveTab = "GLTF";
+                }
+            });
+        }
     });
 
     // Resize
@@ -241,7 +254,15 @@ if (BABYLON.Engine.isSupported()) {
         loadFromAssetUrl();
     }
     else {
-        filesInput = new BABYLON.FilesInput(engine, null, sceneLoaded, null, null, null, function () { BABYLON.Tools.ClearLogCache() }, null, sceneError);
+        var startProcessingFiles = function () {
+            BABYLON.Tools.ClearLogCache();
+
+            if (currentScene) {
+                debugLayerLastActiveTab = currentScene.debugLayer.getActiveTab();
+            }
+        };
+
+        filesInput = new BABYLON.FilesInput(engine, null, sceneLoaded, null, null, null, startProcessingFiles, null, sceneError);
         filesInput.onProcessFileCallback = (function (file, name, extension) {
             if (filesInput._filesToLoad && filesInput._filesToLoad.length === 1 && extension) {
                 if (extension.toLowerCase() === "dds" || extension.toLowerCase() === "env") {

+ 1 - 0
tests/validation/validate.html

@@ -6,6 +6,7 @@
     <script src="https://preview.babylonjs.com/draco_decoder.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>