浏览代码

Update glTF loader to handle texture instancing better

Gary Hsu 7 年之前
父节点
当前提交
b2f393bc17

+ 9 - 20
loaders/src/glTF/2.0/babylon.glTFLoader.ts

@@ -1473,8 +1473,9 @@ module BABYLON.GLTF2 {
             babylonTexture.coordinatesIndex = textureInfo.texCoord || 0;
 
             const image = GLTFLoader._GetProperty(`${context}/source`, this._gltf.images, texture.source);
-            promises.push(this._loadImageAsync(`#/images/${image._index}`, image).then(objectURL => {
-                babylonTexture.updateURL(objectURL);
+            promises.push(this._loadImageAsync(`#/images/${image._index}`, image).then(blob => {
+                const dataUrl = `data:${this._rootUrl}${image.uri || `image${image._index}`}`;
+                babylonTexture.updateURL(dataUrl, blob);
             }));
 
             assign(babylonTexture);
@@ -1496,9 +1497,9 @@ module BABYLON.GLTF2 {
             return sampler._data;
         }
 
-        private _loadImageAsync(context: string, image: _ILoaderImage): Promise<string> {
-            if (image._objectURL) {
-                return image._objectURL;
+        private _loadImageAsync(context: string, image: _ILoaderImage): Promise<Blob> {
+            if (image._blob) {
+                return image._blob;
             }
 
             let promise: Promise<ArrayBufferView>;
@@ -1510,11 +1511,11 @@ module BABYLON.GLTF2 {
                 promise = this._loadBufferViewAsync(`#/bufferViews/${bufferView._index}`, bufferView);
             }
 
-            image._objectURL = promise.then(data => {
-                return URL.createObjectURL(new Blob([data], { type: image.mimeType }));
+            image._blob = promise.then(data => {
+                return new Blob([data], { type: image.mimeType });
             });
 
-            return image._objectURL;
+            return image._blob;
         }
 
         /** @hidden */
@@ -1749,18 +1750,6 @@ module BABYLON.GLTF2 {
 
             this._requests.length = 0;
 
-            if (this._gltf && this._gltf.images) {
-                for (const image of this._gltf.images) {
-                    if (image._objectURL) {
-                        image._objectURL.then(value => {
-                            URL.revokeObjectURL(value);
-                        });
-
-                        image._objectURL = undefined;
-                    }
-                }
-            }
-
             delete this._gltf;
             delete this._babylonScene;
             this._completePromises.length = 0;

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

@@ -49,7 +49,7 @@ module BABYLON.GLTF2 {
 
     /** @hidden */
     export interface _ILoaderImage extends IImage, _IArrayItem {
-        _objectURL?: Promise<string>;
+        _blob?: Promise<Blob>;
     }
 
     /** @hidden */

+ 9 - 6
src/Engine/babylon.engine.ts

@@ -1015,7 +1015,7 @@
         protected _alphaMode = Engine.ALPHA_DISABLE;
 
         // Cache
-        private _internalTexturesCache = new Array<InternalTexture>();
+        protected _internalTexturesCache = new Array<InternalTexture>();
         /** @hidden */
         protected _activeChannel = 0;
         private _currentTextureChannel = -1;    
@@ -4113,14 +4113,14 @@
          * @param samplingMode mode with should be used sample / access the texture (Default: BABYLON.Texture.TRILINEAR_SAMPLINGMODE)
          * @param onLoad optional callback to be called upon successful completion
          * @param onError optional callback to be called upon failure
-         * @param buffer a source of a file previously fetched as either an ArrayBuffer (compressed or image format) or HTMLImageElement (image format)
+         * @param buffer a source of a file previously fetched as either a base64 string, an ArrayBuffer (compressed or image format), HTMLImageElement (image format), or a Blob
          * @param fallback an internal argument in case the function must be called again, due to etc1 not having alpha capabilities
          * @param format internal format.  Default: RGB when extension is '.jpg' else RGBA.  Ignored for compressed textures
          * @returns a InternalTexture for assignment back into BABYLON.Texture
          */
         public createTexture(urlArg: Nullable<string>, noMipmap: boolean, invertY: boolean, scene: Nullable<Scene>, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE,
             onLoad: Nullable<() => void> = null, onError: Nullable<(message: string, exception: any) => void> = null,
-            buffer: Nullable<ArrayBuffer | HTMLImageElement> = null, fallback: Nullable<InternalTexture> = null, format: Nullable<number> = null): InternalTexture {
+            buffer: Nullable<string | ArrayBuffer | HTMLImageElement | Blob> = null, fallback: Nullable<InternalTexture> = null, format: Nullable<number> = null): InternalTexture {
             var url = String(urlArg); // assign a new string, so that the original is still available in case of fallback
             var fromData = url.substr(0, 5) === "data:";
             var fromBlob = url.substr(0, 5) === "blob:";
@@ -4272,16 +4272,19 @@
                     }, samplingMode);
                 };
 
-                if (!fromData || isBase64)
+                if (!fromData || isBase64) {
                     if (buffer instanceof HTMLImageElement) {
                         onload(buffer);
                     } else {
                         Tools.LoadImage(url, onload, onerror, scene ? scene.database : null);
                     }
-                else if (buffer instanceof Array || typeof buffer === "string" || buffer instanceof ArrayBuffer)
+                }
+                else if (typeof buffer === "string" || buffer instanceof ArrayBuffer || buffer instanceof Blob) {
                     Tools.LoadImage(buffer, onload, onerror, scene ? scene.database : null);
-                else
+                }
+                else {
                     onload(<HTMLImageElement>buffer);
+                }
             }
 
             return texture;

+ 5 - 0
src/Engine/babylon.nullEngine.ts

@@ -315,6 +315,8 @@
                 onLoad();
             }
 
+            this._internalTexturesCache.push(texture);
+
             return texture;
         }
 
@@ -352,6 +354,9 @@
             texture.type = fullOptions.type;
             texture._generateDepthBuffer = fullOptions.generateDepthBuffer;
             texture._generateStencilBuffer = fullOptions.generateStencilBuffer ? true : false;
+
+            this._internalTexturesCache.push(texture);
+
             return texture;
         }
 

+ 1 - 1
src/Materials/Textures/babylon.internalTexture.ts

@@ -140,7 +140,7 @@ module BABYLON {
         /** @hidden */
         public _dataSource = InternalTexture.DATASOURCE_UNKNOWN;
         /** @hidden */
-        public _buffer: Nullable<ArrayBuffer | HTMLImageElement>;
+        public _buffer: Nullable<string | ArrayBuffer | HTMLImageElement | Blob>;
         /** @hidden */
         public _bufferView: Nullable<ArrayBufferView>;
         /** @hidden */

+ 14 - 4
src/Materials/Textures/babylon.texture.ts

@@ -106,7 +106,7 @@
         private _cachedProjectionMatrixId: number;
         private _cachedCoordinatesMode: number;
         public _samplingMode: number;
-        private _buffer: any;
+        private _buffer: Nullable<string | ArrayBuffer | HTMLImageElement | Blob>;
         private _deleteBuffer: boolean;
         protected _format: Nullable<number>;
         private _delayedOnLoad: Nullable<() => void>;
@@ -126,7 +126,7 @@
             return this._samplingMode;
         }
 
-        constructor(url: Nullable<string>, scene: Nullable<Scene>, noMipmap: boolean = false, invertY: boolean = true, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, onLoad: Nullable<() => void> = null, onError: Nullable<(message?: string, exception?: any) => void> = null, buffer: any = null, deleteBuffer: boolean = false, format?: number) {
+        constructor(url: Nullable<string>, scene: Nullable<Scene>, noMipmap: boolean = false, invertY: boolean = true, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, onLoad: Nullable<() => void> = null, onError: Nullable<(message?: string, exception?: any) => void> = null, buffer: Nullable<string | ArrayBuffer | HTMLImageElement | Blob> = null, deleteBuffer: boolean = false, format?: number) {
             super(scene);
 
             this.name = url || "";
@@ -189,8 +189,18 @@
             }
         }
 
-        public updateURL(url: string): void {
+        /**
+         * Update the url (and optional buffer) of this texture if url was null during construction.
+         * @param url the url of the texture
+         * @param buffer the buffer of the texture (defaults to null)
+         */
+        public updateURL(url: string, buffer: Nullable<string | ArrayBuffer | HTMLImageElement | Blob> = null): void {
+            if (this.url) {
+                throw new Error("URL is already set");
+            }
+
             this.url = url;
+            this._buffer = buffer;
             this.delayLoadState = Engine.DELAYLOADSTATE_NOTLOADED;
             this.delayLoad();
         }
@@ -400,7 +410,7 @@
         public serialize(): any {
             var serializationObject = super.serialize();
 
-            if (typeof this._buffer === "string" && this._buffer.substr(0, 5) === "data:") {
+            if (typeof this._buffer === "string" && (this._buffer as string).substr(0, 5) === "data:") {
                 serializationObject.base64String = this._buffer;
                 serializationObject.name = serializationObject.name.replace("data:", "");
             }

+ 34 - 10
src/Tools/babylon.tools.ts

@@ -499,32 +499,56 @@
             return url;
         }
 
-        public static LoadImage(url: any, onLoad: (img: HTMLImageElement) => void, onError: (message?: string, exception?: any) => void, database: Nullable<Database>): HTMLImageElement {
-            if (url instanceof ArrayBuffer) {
-                url = Tools.EncodeArrayBufferTobase64(url);
-            }
-
-            url = Tools.CleanUrl(url);
+        /**
+         * Loads an image as an HTMLImageElement.
+         * @param input url string, ArrayBuffer, or Blob to load
+         * @param onLoad callback called when the image successfully loads
+         * @param onError callback called when the image fails to load
+         * @param database database for caching
+         * @returns the HTMLImageElement of the loaded image
+         */
+        public static LoadImage(input: string | ArrayBuffer | Blob, onLoad: (img: HTMLImageElement) => void, onError: (message?: string, exception?: any) => void, database: Nullable<Database>): HTMLImageElement {
+            let url: string;
+            let usingObjectURL = false;
 
-            url = Tools.PreprocessUrl(url);
+            if (input instanceof ArrayBuffer) {
+                url = URL.createObjectURL(new Blob([input]));
+                usingObjectURL = true;
+            }
+            else if (input instanceof Blob) {
+                url = URL.createObjectURL(input);
+                usingObjectURL = true;
+            }
+            else {
+                url = Tools.CleanUrl(input);
+                url = Tools.PreprocessUrl(input);
+            }
 
             var img = new Image();
             Tools.SetCorsBehavior(url, img);
 
             const loadHandler = () => {
+                if (usingObjectURL && img.src) {
+                    URL.revokeObjectURL(img.src);
+                }
+
                 img.removeEventListener("load", loadHandler);
                 img.removeEventListener("error", errorHandler);
                 onLoad(img);
             };
 
             const errorHandler = (err: any) => {
+                if (usingObjectURL && img.src) {
+                    URL.revokeObjectURL(img.src);
+                }
+
                 img.removeEventListener("load", loadHandler);
                 img.removeEventListener("error", errorHandler);
 
-                Tools.Error("Error while trying to load image: " + url);
+                Tools.Error("Error while trying to load image: " + input);
 
                 if (onError) {
-                    onError("Error while trying to load image: " + url, err);
+                    onError("Error while trying to load image: " + input, err);
                 }
             };
 
@@ -541,7 +565,6 @@
                 }
             };
 
-
             //ANY database to do!
             if (url.substr(0, 5) !== "data:" && database && database.enableTexturesOffline && Database.IsUASupportingBlobStorage) {
                 database.openAsync(loadFromIndexedDB, noIndexedDB);
@@ -560,6 +583,7 @@
                                 blobURL = URL.createObjectURL(FilesInput.FilesToLoad[textureName]);
                             }
                             img.src = blobURL;
+                            usingObjectURL = true;
                         }
                         catch (e) {
                             img.src = "";

+ 12 - 0
tests/unit/babylon/src/Loading/babylon.sceneLoader.tests.ts

@@ -415,6 +415,18 @@ describe('Babylon Scene Loader', function () {
             return Promise.all(promises);
         });
 
+        it('Load BoomBox twice and check texture instancing', () => {
+            const scene = new BABYLON.Scene(subject);
+            return BABYLON.SceneLoader.AppendAsync("/Playground/scenes/BoomBox/", "BoomBox.gltf", scene).then(() => {
+                const createTextureSpy = sinon.spy(subject, "createTexture");
+                return BABYLON.SceneLoader.AppendAsync("/Playground/scenes/BoomBox/", "BoomBox.gltf", scene).then(() => {
+                    const called = createTextureSpy.called;
+                    createTextureSpy.restore();
+                    expect(called, "createTextureSpyCalled").to.be.false;
+                });
+            });
+        });
+
         // TODO: test animation group callback
         // TODO: test material instancing
         // TODO: test ImportMesh with specific node name