浏览代码

Merge pull request #1643 from Palmer-JC/master

Intelligent Texture Selection fully implemented
David Catuhe 8 年之前
父节点
当前提交
67cfc748c8
共有 3 个文件被更改,包括 251 次插入63 次删除
  1. 1 0
      Tools/Gulp/config.json
  2. 142 0
      src/Tools/babylon.khronosTextureContainer.ts
  3. 108 63
      src/babylon.engine.ts

+ 1 - 0
Tools/Gulp/config.json

@@ -163,6 +163,7 @@
       "../../src/Materials/Textures/babylon.mapTexture.js",
       "../../src/Materials/babylon.shaderMaterial.js",
       "../../src/Tools/babylon.tools.dds.js",
+      "../../src/Tools/babylon.khronosTextureContainer.js",
       "../../src/Physics/Plugins/babylon.cannonJSPlugin.js",
       "../../src/Physics/Plugins/babylon.oimoJSPlugin.js",
       "../../src/PostProcess/babylon.displayPassPostProcess.js",

+ 142 - 0
src/Tools/babylon.khronosTextureContainer.ts

@@ -0,0 +1,142 @@
+module BABYLON.Internals {
+    /**
+     * for description see https://www.khronos.org/opengles/sdk/tools/KTX/
+     * for file layout see https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
+     */
+    export class KhronosTextureContainer {
+        static HEADER_LEN = 12 + (13 * 4); // identifier + header elements (not including key value meta-data pairs)
+        
+        // load types
+        static COMPRESSED_2D = 0; // uses a gl.compressedTexImage2D()
+        static COMPRESSED_3D = 1; // uses a gl.compressedTexImage3D()
+        static TEX_2D        = 2; // uses a gl.texImage2D()
+        static TEX_3D        = 3; // uses a gl.texImage3D()
+        
+        // elements of the header 
+        public glType : number;
+        public glTypeSize : number;
+        public glFormat : number;
+        public glInternalFormat : number;
+        public glBaseInternalFormat : number;
+        public pixelWidth : number;
+        public pixelHeight : number;
+        public pixelDepth : number;
+        public numberOfArrayElements : number;
+        public numberOfFaces : number;
+        public numberOfMipmapLevels : number;
+        public bytesOfKeyValueData : number;
+        
+        public loadType : number;
+        /**
+         * @param {ArrayBuffer} arrayBuffer- contents of the KTX container file
+         * @param {number} facesExpected- should be either 1 or 6, based whether a cube texture or or
+         * @param {boolean} threeDExpected- provision for indicating that data should be a 3D texture, not implemented
+         * @param {boolean} textureArrayExpected- provision for indicating that data should be a texture array, not implemented
+         */
+        public constructor (public arrayBuffer : any, facesExpected : number, threeDExpected? : boolean, textureArrayExpected? : boolean) {
+            // Test that it is a ktx formatted file, based on the first 12 bytes, character representation is:
+            // '«', 'K', 'T', 'X', ' ', '1', '1', '»', '\r', '\n', '\x1A', '\n'
+            // 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
+            var identifier = new Uint8Array(this.arrayBuffer, 0, 12);
+            if (identifier[ 0] !== 0xAB || identifier[ 1] !== 0x4B || identifier[ 2] !== 0x54 || identifier[ 3] !== 0x58 || identifier[ 4] !== 0x20 || identifier[ 5] !== 0x31 || 
+                identifier[ 6] !== 0x31 || identifier[ 7] !== 0xBB || identifier[ 8] !== 0x0D || identifier[ 9] !== 0x0A || identifier[10] !== 0x1A || identifier[11] !== 0x0A) {
+                Tools.Error("texture missing KTX identifier");
+                return;
+            }
+            
+            // load the reset of the header in native 32 bit int
+            var header = new Int32Array(this.arrayBuffer, 12, 13);
+            
+            // determine of the remaining header values are recorded in the opposite endianness & require conversion
+            var oppositeEndianess = header[0] === 0x01020304;
+            
+            // read all the header elements in order they exist in the file, without modification (sans endainness)
+            this.glType                = oppositeEndianess ? this.switchEndainness(header[ 1]) : header[ 1]; // must be 0 for compressed textures
+            this.glTypeSize            = oppositeEndianess ? this.switchEndainness(header[ 2]) : header[ 2]; // must be 1 for compressed textures
+            this.glFormat              = oppositeEndianess ? this.switchEndainness(header[ 3]) : header[ 3]; // must be 0 for compressed textures
+            this.glInternalFormat      = oppositeEndianess ? this.switchEndainness(header[ 4]) : header[ 4]; // the value of arg passed to gl.compressedTexImage2D(,,x,,,,)
+            this.glBaseInternalFormat  = oppositeEndianess ? this.switchEndainness(header[ 5]) : header[ 5]; // specify GL_RGB, GL_RGBA, GL_ALPHA, etc (un-compressed only)
+            this.pixelWidth            = oppositeEndianess ? this.switchEndainness(header[ 6]) : header[ 6]; // level 0 value of arg passed to gl.compressedTexImage2D(,,,x,,,)
+            this.pixelHeight           = oppositeEndianess ? this.switchEndainness(header[ 7]) : header[ 7]; // level 0 value of arg passed to gl.compressedTexImage2D(,,,,x,,)
+            this.pixelDepth            = oppositeEndianess ? this.switchEndainness(header[ 8]) : header[ 8]; // level 0 value of arg passed to gl.compressedTexImage3D(,,,,,x,,)
+            this.numberOfArrayElements = oppositeEndianess ? this.switchEndainness(header[ 9]) : header[ 9]; // used for texture arrays
+            this.numberOfFaces         = oppositeEndianess ? this.switchEndainness(header[10]) : header[10]; // used for cubemap textures, should either be 1 or 6
+            this.numberOfMipmapLevels  = oppositeEndianess ? this.switchEndainness(header[11]) : header[11]; // number of levels; disregard possibility of 0 for compressed textures
+            this.bytesOfKeyValueData   = oppositeEndianess ? this.switchEndainness(header[12]) : header[12]; // the amount of space after the header for meta-data
+            
+            // Make sure we have a compressed type.  Not only reduces work, but probably better to let dev know they are not compressing.
+            if (this.glType !== 0) {
+                Tools.Error("only compressed formats currently supported");
+                return;
+            } else {
+                // value of zero is an indication to generate mipmaps @ runtime.  Not usually allowed for compressed, so disregard.
+                this.numberOfMipmapLevels = Math.max(1, this.numberOfMipmapLevels);
+            }
+            
+            if (this.pixelHeight === 0 || this.pixelDepth !== 0) {
+                Tools.Error("only 2D textures currently supported");
+                return;
+            }
+            
+            if (this.numberOfArrayElements !== 0) {
+                Tools.Error("texture arrays not currently supported");
+                return;
+            }
+            
+            if (this.numberOfFaces !== facesExpected) {
+                Tools.Error("number of faces expected" + facesExpected + ", but found " + this.numberOfFaces);
+                return;
+            }
+              
+            // we now have a completely validated file, so could use existence of loadType as success
+            // would need to make this more elaborate & adjust checks above to support more than one load type
+            this.loadType = KhronosTextureContainer.COMPRESSED_2D;
+        }
+        
+        // not as fast hardware based, but will probably never need to use
+        public switchEndainness(val : number) : number {
+            return ((val & 0xFF) << 24)
+                 | ((val & 0xFF00) << 8)
+                 | ((val >> 8) & 0xFF00)
+                 | ((val >> 24) & 0xFF);            
+        }
+        
+        /**
+         * It is assumed that the texture has already been created & is currently bound
+         */
+        public uploadLevels(gl: WebGLRenderingContext, loadMipmaps: boolean) : void {
+            switch (this.loadType){
+                case KhronosTextureContainer.COMPRESSED_2D:
+                    this._upload2DCompressedLevels(gl, loadMipmaps);
+                    break;
+                    
+                case KhronosTextureContainer.TEX_2D:
+                case KhronosTextureContainer.COMPRESSED_3D:
+                case KhronosTextureContainer.TEX_3D:
+            }
+        }
+        
+        private _upload2DCompressedLevels(gl: WebGLRenderingContext, loadMipmaps: boolean): void {
+            // initialize width & height for level 1
+            var dataOffset = KhronosTextureContainer.HEADER_LEN + this.bytesOfKeyValueData;
+            var width = this.pixelWidth;
+            var height = this.pixelHeight;
+            
+            var mipmapCount = loadMipmaps ? this.numberOfMipmapLevels : 1;
+            for (var level = 0; level < mipmapCount; level++) {
+                var imageSize = new Int32Array(this.arrayBuffer, dataOffset, 1)[0]; // size per face, since not supporting array cubemaps
+                
+                for (var face = 0; face < this.numberOfFaces; face++) {
+                    var sampler = this.numberOfFaces === 1 ? gl.TEXTURE_2D : (gl.TEXTURE_CUBE_MAP_POSITIVE_X + face);
+                    var byteArray = new Uint8Array(this.arrayBuffer, dataOffset + 4, imageSize);
+                    gl.compressedTexImage2D(sampler, level, this.glInternalFormat, width, height, 0, byteArray);
+
+                    dataOffset += imageSize + 4; // size of the image + 4 for the imageSize field
+                    dataOffset += 3 - ((imageSize + 3) % 4); // add padding for odd sized image
+                }
+                width = Math.max(1.0, width * 0.5);
+                height = Math.max(1.0, height * 0.5);
+            }
+        }
+    }
+} 

+ 108 - 63
src/babylon.engine.ts

@@ -181,6 +181,7 @@
         public etc1: any; //WEBGL_compressed_texture_etc1;
         public etc2: any; //WEBGL_compressed_texture_etc;
         public astc: any; //WEBGL_compressed_texture_astc;
+        public atc: any; //WEBGL_compressed_texture_atc;
         public textureFloat: boolean;
         public textureAnisotropicFilterExtension: EXT_texture_filter_anisotropic;
         public maxAnisotropy: number;
@@ -604,11 +605,13 @@
             // Extensions
             this._caps.standardDerivatives = (this._gl.getExtension('OES_standard_derivatives') !== null);
             
-            this._caps.astc  = this._gl.getExtension('WEBGL_compressed_texture_astc');
-            this._caps.s3tc  = this._gl.getExtension('WEBGL_compressed_texture_s3tc');
-            this._caps.pvrtc = this._gl.getExtension('WEBGL_compressed_texture_pvrtc') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'); // 2nd is what iOS reports
-            this._caps.etc1  = this._gl.getExtension('WEBGL_compressed_texture_etc1');
-            this._caps.etc2  = this._gl.getExtension('WEBGL_compressed_texture_etc') || this._gl.getExtension('WEBGL_compressed_texture_es3_0'); // first is the final name, found hardware using 2nd
+            this._caps.astc  = this._gl.getExtension('WEBGL_compressed_texture_astc' ) || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_astc' );
+            this._caps.s3tc  = this._gl.getExtension('WEBGL_compressed_texture_s3tc' ) || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc' );
+            this._caps.pvrtc = this._gl.getExtension('WEBGL_compressed_texture_pvrtc') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc');
+            this._caps.etc1  = this._gl.getExtension('WEBGL_compressed_texture_etc1' ) || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_etc1' );
+            this._caps.etc2  = this._gl.getExtension('WEBGL_compressed_texture_etc'  ) || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_etc'  ) ||
+                               this._gl.getExtension('WEBGL_compressed_texture_es3_0'); // also a requirement of OpenGL ES 3
+            this._caps.atc   = this._gl.getExtension('WEBGL_compressed_texture_atc'  ) || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_atc'  );
             
             this._caps.textureFloat = (this._gl.getExtension('OES_texture_float') !== null);
             this._caps.textureAnisotropicFilterExtension = this._gl.getExtension('EXT_texture_filter_anisotropic') || this._gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic') || this._gl.getExtension('MOZ_EXT_texture_filter_anisotropic');
@@ -626,16 +629,18 @@
             this._caps.textureHalfFloatLinearFiltering = this._gl.getExtension('OES_texture_half_float_linear');
             this._caps.textureHalfFloatRender = renderToHalfFloat;
             
-            // Intelligently add supported commpressed formats in order to check for.
+            // Intelligently add supported compressed formats in order to check for.
             // Check for ASTC support first as it is most powerful and to be very cross platform.
-            // Next PVR & S3, which are probably superior to ETC1/2.  
-            // Likely no hardware which supports both PVR & S3, so order matters little.
-            // ETC2 is newer and handles ETC1, so check for first.
-            if (this._caps.astc ) this.texturesSupported.push('.astc');
-            if (this._caps.s3tc ) this.texturesSupported.push('.dds');
-            if (this._caps.pvrtc) this.texturesSupported.push('.pvr');
-            if (this._caps.etc2 ) this.texturesSupported.push('.etc2');
-            if (this._caps.etc1 ) this.texturesSupported.push('.etc1');
+            // Next PVRTC & DXT, which are probably superior to ETC1/2.  
+            // Likely no hardware which supports both PVR & DXT, so order matters little.
+            // ETC2 is newer and handles ETC1 (no alpha capability), so check for first.
+            // ATC before ETC1, since both old (widely supported), but ATC supports alpha, but ETC1 does not
+            if (this._caps.astc ) this.texturesSupported.push('-astc.ktx');
+            if (this._caps.s3tc ) this.texturesSupported.push('-dxt.ktx');
+            if (this._caps.pvrtc) this.texturesSupported.push('-pvrtc.ktx');
+            if (this._caps.etc2 ) this.texturesSupported.push('-etc2.ktx');
+            if (this._caps.atc  ) this.texturesSupported.push('-atc.ktx');
+            if (this._caps.etc1 ) this.texturesSupported.push('-etc1.ktx');
             
             if (this._gl.getShaderPrecisionFormat) {
                 var highp = this._gl.getShaderPrecisionFormat(this._gl.FRAGMENT_SHADER, this._gl.HIGH_FLOAT);
@@ -1950,25 +1955,28 @@
             texture.samplingMode = samplingMode;
         }
         /**
-         * Set the compressed texture format to use, based on the formats you have,
-         * the formats supported by the hardware / browser, and those currently implemented
-         * in BJS.
+         * Set the compressed texture format to use, based on the formats you have, and the formats
+         * supported by the hardware / browser.
          * 
-         * Note: The result of this call is not taken into account texture is base64 or when
+         * Khronos Texture Container (.ktx) files are used to support this.  This format has the
+         * advantage of being specifically designed for OpenGL.  Header elements directly correspond
+         * to API arguments needed to compressed textures.  This puts the burden on the container
+         * generator to house the arcane code for determining these for current & future formats.
+         * 
+         * for description see https://www.khronos.org/opengles/sdk/tools/KTX/
+         * for file layout see https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
+         * 
+         * Note: The result of this call is not taken into account when a texture is base64 or when
          * using a database / manifest.
          * 
-         * @param {Array<string>} formatsAvailable - Extension names including dot.  Case
-         * and order do not matter.
+         * @param {Array<string>} formatsAvailable- The list of those format families you have created
+         * on your server.  Syntax: '-' + format family + '.ktx'.  (Case and order do not matter.)
+         * 
+         * Current families are astc, dxt, pvrtc, etc2, atc, & etc1.
          * @returns The extension selected.
          */
         public setTextureFormatToUse(formatsAvailable : Array<string>) : string {
             for (var i = 0, len1 = this.texturesSupported.length; i < len1; i++) {
-                // code to allow the formats to be added as they can be developed / hw tested
-                if (this._texturesSupported[i] === '.astc') continue;
-                if (this._texturesSupported[i] === '.pvr' ) continue;
-                if (this._texturesSupported[i] === '.etc1') continue;
-                if (this._texturesSupported[i] === '.etc2') continue;
-                
                 for (var j = 0, len2 = formatsAvailable.length; j < len2; j++) {
                     if (this._texturesSupported[i] === formatsAvailable[j].toLowerCase()) {
                         return this._textureFormatInUse = this._texturesSupported[i];
@@ -1984,6 +1992,7 @@
             var texture = this._gl.createTexture();
 
             var extension: string;
+            var isKTX = false;
             var fromData: any = false;
             if (url.substr(0, 5) === "data:") {
                 fromData = true;
@@ -1995,6 +2004,7 @@
                 if (this._textureFormatInUse && !fromData && !scene.database) {
                     extension = this._textureFormatInUse;
                     url = url.substring(0, lastDot) + this._textureFormatInUse;
+                    isKTX = true;
                 }
             } else {
                 var oldUrl = url;
@@ -2022,40 +2032,44 @@
                 }
             };
             var callback: (arrayBuffer: any) => void;
-            if (isTGA) {
-                callback = (arrayBuffer) => {
-                    var data = new Uint8Array(arrayBuffer);
-
-                    var header = Internals.TGATools.GetTGAHeader(data);
-
-                    prepareWebGLTexture(texture, this._gl, scene, header.width, header.height, invertY, noMipmap, false, () => {
-                        Internals.TGATools.UploadContent(this._gl, data);
-                    }, samplingMode);
-                };
-                if (!(fromData instanceof Array))
-                    Tools.LoadFile(url, arrayBuffer => {
-                        callback(arrayBuffer);
-                    }, null, scene.database, true, onerror);
-                else
-                    callback(buffer);
-
-            } else if (isDDS) {
-                callback = (data) => {
-                    var info = Internals.DDSTools.GetDDSInfo(data);
-
-                    var loadMipmap = (info.isRGB || info.isLuminance || info.mipmapCount > 1) && !noMipmap && ((info.width >> (info.mipmapCount - 1)) === 1);
-                    prepareWebGLTexture(texture, this._gl, scene, info.width, info.height, invertY, !loadMipmap, info.isFourCC, () => {
-
-                        Internals.DDSTools.UploadDDSLevels(this._gl, this.getCaps().s3tc, data, info, loadMipmap, 1);
-                    }, samplingMode);
-                };
-
-                if (!(fromData instanceof Array))
-                    Tools.LoadFile(url, data => {
-                        callback(data);
-                    }, null, scene.database, true, onerror);
-                else
-                    callback(buffer);
+            if (isKTX || isTGA || isDDS){
+                if (isKTX) {
+                    callback = (data) => {
+                        var ktx = new Internals.KhronosTextureContainer(data, 1);
+    
+                        prepareWebGLTexture(texture, this._gl, scene, ktx.pixelWidth, ktx.pixelHeight, invertY, false, true, () => {
+                            ktx.uploadLevels(this._gl, !noMipmap);
+                        }, samplingMode);
+                    };
+                } else if (isTGA) {
+                    callback = (arrayBuffer) => {
+                        var data = new Uint8Array(arrayBuffer);
+
+                        var header = Internals.TGATools.GetTGAHeader(data);
+
+                        prepareWebGLTexture(texture, this._gl, scene, header.width, header.height, invertY, noMipmap, false, () => {
+                            Internals.TGATools.UploadContent(this._gl, data);
+                        }, samplingMode);
+                    };
+
+                } else if (isDDS) {
+                    callback = (data) => {
+                        var info = Internals.DDSTools.GetDDSInfo(data);
+
+                        var loadMipmap = (info.isRGB || info.isLuminance || info.mipmapCount > 1) && !noMipmap && ((info.width >> (info.mipmapCount - 1)) === 1);
+                        prepareWebGLTexture(texture, this._gl, scene, info.width, info.height, invertY, !loadMipmap, info.isFourCC, () => {
+
+                            Internals.DDSTools.UploadDDSLevels(this._gl, this.getCaps().s3tc, data, info, loadMipmap, 1);
+                        }, samplingMode);
+                    };
+            }
+
+            if (!(fromData instanceof Array))
+                Tools.LoadFile(url, data => {
+                    callback(data);
+                }, null, scene.database, true, onerror);
+            else
+                callback(buffer);
 
             } else {
                 var onload = (img) => {
@@ -2495,10 +2509,41 @@
             texture.references = 1;
             texture.onLoadedCallbacks = [];
 
-            var extension = rootUrl.substr(rootUrl.length - 4, 4).toLowerCase();
-            var isDDS = this.getCaps().s3tc && (extension === ".dds");
+            var isKTX = false;
+            var lastDot = rootUrl.lastIndexOf('.');
+            var extension = rootUrl.substring(lastDot).toLowerCase();
+            if (this._textureFormatInUse && !scene.database) {
+                extension = this._textureFormatInUse;
+                rootUrl = rootUrl.substring(0, lastDot) + this._textureFormatInUse;
+                isKTX = true;
+            }
+            var isDDS = (extension === ".dds");
+
+            if (isKTX) {
+                Tools.LoadFile(rootUrl, data => {
+                    var ktx = new Internals.KhronosTextureContainer(data, 6);
+
+                    var loadMipmap = ktx.numberOfMipmapLevels > 1 && !noMipmap;
+
+                    this._bindTextureDirectly(gl.TEXTURE_CUBE_MAP, texture);
+                    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
+    
+                    ktx.uploadLevels(this._gl, !noMipmap);
 
-            if (isDDS) {
+                    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+                    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, loadMipmap ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
+                    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+                    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+
+                    this._bindTextureDirectly(gl.TEXTURE_CUBE_MAP, null);
+
+                    this.resetTextureCache();
+
+                    texture._width = ktx.pixelWidth;
+                    texture._height = ktx.pixelHeight;
+                    texture.isReady = true;
+                }, null, null, true, onError);
+            } else if (isDDS) {
                 Tools.LoadFile(rootUrl, data => {
                     var info = Internals.DDSTools.GetDDSInfo(data);