import { assert , log , LogLevel } from './auxiliaries' ; import { byteSizeOfFormat } from './formatbytesizes' ; import { Bindable } from './bindable' ; import { TexImage2DData } from './gl2facade' ; import { Initializable } from './initializable' ; import { AbstractObject } from './object' ; /**
 * Wrapper for an WebGL cube texture providing size accessors and requiring for bind, unbind, resize, validity, and
 * initialization implementations. The texture cube object is created on initialization and deleted on uninitialization.
 * After being initialized, the texture cube can be resized, reformated, and data can set directly or via fetch:
 * ```
 * const cubeMap = new TextureCube(context, 'CubeMap');
 * cubeMap.initialize(512, gl.RGB8, gl.RGB, gl.UNSIGNED_BYTE);
 * cubeMap.fetch({
 *     positiveX: 'data/cubemap.px.png', negativeX: 'data/cubemap.nx.png',
 *     positiveY: 'data/cubemap.py.png', negativeY: 'data/cubemap.ny.png',
 *     positiveZ: 'data/cubemap.pz.png', negativeZ: 'data/cubemap.nz.png',
 * }).then(() => this.invalidate(true);
 * ```
 * Please note that each of the six textures of a texture cube is required to be of the exact same, square dimensions.
 * This is reflected within this classes interface by providing a single size property in favor to width and height.
 */ export class TextureCube extends AbstractObject < WebGLTexture > implements Bindable { /**
     * Default texture, e.g., used for unbind.
     */ static readonly DEFAULT_TEXTURE = undefined ; see size protected _size : GLsizei = 0 ; see internalFormat protected _internalFormat : GLenum = 0 ; see format protected _format : GLenum = 0 ; see type protected _type : GLenum = 0 ; /**
     * For tracking approximate use of GPU storage in bytes per face.
     */ protected _bytes : Array < GLsizei > = [ 0 , 0 , 0 , 0 , 0 , 0 ] ; param face returns protected faceID ( face ) { const gl = this . _context . gl ; switch ( face ) { case gl . TEXTURE_CUBE_MAP_POSITIVE_X : return 0 ; case gl . TEXTURE_CUBE_MAP_NEGATIVE_X : return 1 ; case gl . TEXTURE_CUBE_MAP_POSITIVE_Y : return 2 ; case gl . TEXTURE_CUBE_MAP_NEGATIVE_Y : return 3 ; case gl . TEXTURE_CUBE_MAP_POSITIVE_Z : return 4 ; case gl . TEXTURE_CUBE_MAP_NEGATIVE_Z : return 5 ; default : assert ( false , `expected texture cube map identifier (${ gl . TEXTURE_CUBE_MAP_POSITIVE_X }, ` + `${ gl . TEXTURE_CUBE_MAP_NEGATIVE_X }, ${ gl . TEXTURE_CUBE_MAP_POSITIVE_Y }, ` + `${ gl . TEXTURE_CUBE_MAP_NEGATIVE_Y }, ${ gl . TEXTURE_CUBE_MAP_POSITIVE_Z }, or` + `${ gl . TEXTURE_CUBE_MAP_NEGATIVE_Z }), given ${ face }` ) ; return - 1 ; } } param size param internalFormat param format param type protected create ( size , internalFormat , format , type ) { assert ( size > 0 , `texture cube requires valid size (width/height) of greater than zero` ) ; const gl = this . _context . gl ; const gl2facade = this . _context . gl2facade ; this . _object = gl . createTexture ( ) ; this . _size = size ; this . _internalFormat = internalFormat ; this . _format = format ; this . _type = type ; gl . bindTexture ( gl . TEXTURE_CUBE_MAP , this . _object ) ; gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_MIN_FILTER , gl . LINEAR ) ; gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_MAG_FILTER , 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 ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_X , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_X , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_Y , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_Y , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_Z , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_Z , 0 , this . _internalFormat , this . _size , this . _size , 0 , this . _format , this . _type ) ; gl . bindTexture ( gl . TEXTURE_CUBE_MAP , TextureCube . DEFAULT_TEXTURE ) ; this . _valid = gl . isTexture ( this . _object ) ; this . context . allocationRegister . reallocate ( this . _identifier , 0 ) ; return this . _object ; } /**
     * Delete the texture object on the GPU. This should have the reverse effect of `create`.
     */ protected delete ( ) { assert ( this . _object instanceof WebGLTexture , `expected WebGLTexture object` ) ; this . _context . gl . deleteTexture ( this . _object ) ; this . _object = undefined ; this . _valid = false ; this . _internalFormat = 0 ; this . _format = 0 ; this . _type = 0 ; this . _size = 0 ; } /**
     * Crops the contents of an image and returns a ImageData element of the resulting image.
     * Offsets and sizes of the cropping range need to be supplied. The resulting image data will
     * have the same height and width as the crop size.
     */ protected cropImage ( srcImage , offsetX , offsetY , width , height ) { const canvas = document . createElement ( 'canvas' ) ; canvas . width = width ; canvas . height = height ; const ctx = canvas . getContext ( '2d' ) ; if ( ! ctx ) { console . log ( LogLevel . Warning , '2D context creation failed when cropping image.' ) ; return new ImageData ( 0 , 0 ) ; } ctx . drawImage ( srcImage , offsetX , offsetY , width , height , 0 , 0 , width , height ) ; return ctx . getImageData ( 0 , 0 , width , height ) ; } param image param mipLevel protected extractMipLevelFromAtlas ( image , mipLevel ) { const gl = this . context . gl ; const mip0Size = image . height / 2 ; const mipSize = mip0Size * Math . pow ( 0.5 , mipLevel ) ; const offset = [ 0 , 0 ] ; const incrementOffset = [ image . width , image . height ] ; for ( let i = 0 ; i < mipLevel ; ++ i ) { incrementOffset [ 0 ] /= 2 ; incrementOffset [ 1 ] /= 2 ; offset [ 0 ] += incrementOffset [ 0 ] ; offset [ 1 ] += incrementOffset [ 1 ] ; } /**
         * Flip image along the Y axis, because the Image API and WebGL use
         * different coordinate spaces.
         */ gl . pixelStorei ( gl . UNPACK_FLIP_Y_WEBGL , true ) ; const nxData = this . cropImage ( image , offset [ 0 ] , offset [ 1 ] , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_X , nxData ] , mipLevel ) ; const pzData = this . cropImage ( image , offset [ 0 ] + mipSize , offset [ 1 ] , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_Z , pzData ] , mipLevel ) ; const pxData = this . cropImage ( image , offset [ 0 ] + 2 * mipSize , offset [ 1 ] , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_X , pxData ] , mipLevel ) ; const nzData = this . cropImage ( image , offset [ 0 ] + 3 * mipSize , offset [ 1 ] , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_Z , nzData ] , mipLevel ) ; const pyData = this . cropImage ( image , offset [ 0 ] , offset [ 1 ] + mipSize , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_Y , pyData ] , mipLevel ) ; const nyData = this . cropImage ( image , offset [ 0 ] + mipSize , offset [ 1 ] + mipSize , mipSize , mipSize ) ; this . data ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_Y , nyData ] , mipLevel ) ; gl . pixelStorei ( gl . UNPACK_FLIP_Y_WEBGL , false ) ; } /**
     * Bind the texture object to a texture unit.
     */ @ Initializable . assert_initialized ( ) bind ( unit ? ) { const gl = this . context . gl ; if ( unit ) { gl . activeTexture ( unit ) ; } gl . bindTexture ( gl . TEXTURE_CUBE_MAP , this . _object ) ; } /**
     * Unbind the texture object from a texture unit.
     */ @ Initializable . assert_initialized ( ) unbind ( unit ? ) { const gl = this . context . gl ; if ( unit ) { gl . activeTexture ( unit ) ; } gl . bindTexture ( gl . TEXTURE_CUBE_MAP , TextureCube . DEFAULT_TEXTURE ) ; } param urisByFace param crossOrigin returns @ Initializable . assert_initialized ( ) fetch ( urisByFace , crossOrigin = false , mipLevel = 0 ) { const gl = this . context . gl ; return new Promise ( ( resolve , reject ) => { const images = new Array < [ GLenum , string ] > ( ) ; if ( urisByFace . positiveX ) { images . push ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_X , urisByFace . positiveX ] ) ; } if ( urisByFace . negativeX ) { images . push ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_X , urisByFace . negativeX ] ) ; } if ( urisByFace . positiveY ) { images . push ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_Y , urisByFace . positiveY ] ) ; } if ( urisByFace . negativeY ) { images . push ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_Y , urisByFace . negativeY ] ) ; } if ( urisByFace . positiveZ ) { images . push ( [ gl . TEXTURE_CUBE_MAP_POSITIVE_Z , urisByFace . positiveZ ] ) ; } if ( urisByFace . negativeZ ) { images . push ( [ gl . TEXTURE_CUBE_MAP_NEGATIVE_Z , urisByFace . negativeZ ] ) ; } let waiting = images . length ; for ( const tuple of images ) { const image = new Image ( ) ; image . crossOrigin = 'anonymous' ; image . onerror = ( ) => reject ( ) ; image . onload = ( ) => { const size = this . calculateMipLevelSize ( mipLevel ) ; if ( image . width !== image . height ) { log ( LogLevel . Warning , `image ignored, width and height expected to be equal (square image)` ) ; return ; } if ( image . width !== size ) { log ( LogLevel . Warning , `image ignored, width and height expected to match ` + `this texture's size ${ size }, given ${ image . width }` ) ; return ; } /**
                     * Flip image along the Y axis, because the Image API and WebGL use
                     * different coordinate spaces.
                     */ gl . pixelStorei ( gl . UNPACK_FLIP_Y_WEBGL , true ) ; this . data ( [ tuple [ 0 ] , image ] , mipLevel ) ; gl . pixelStorei ( gl . UNPACK_FLIP_Y_WEBGL , false ) ; waiting = waiting - 1 ; if ( waiting === 0 ) { resolve ( ) ; } } ; if ( crossOrigin ) { image . crossOrigin = 'anonymous' ; } image . src = tuple [ 1 ] ; } } ) ; } param uri param crossOrigin @ Initializable . assert_initialized ( ) fetchMipmapAtlas ( uri , crossOrigin = false ) { return new Promise ( ( resolve , reject ) => { const image = new Image ( ) ; image . onerror = ( ) => reject ( ) ; image . onload = ( ) => { if ( image . width !== image . height * 2 ) { log ( LogLevel . Warning , `Mipmap atlas expected to have dimensions of 2x1.` ) ; return ; } const mip0Size = image . height / 2 ; const numMipLevels = Math . log2 ( mip0Size ) ; for ( let i = 0 ; i < numMipLevels ; ++ i ) { this . extractMipLevelFromAtlas ( image , i ) ; } resolve ( ) ; } ; if ( crossOrigin ) { image . crossOrigin = 'anonymous' ; } image . src = uri ; } ) ; } param level calculateMipLevelSize ( level ) { return this . _size * Math . pow ( 0.5 , level ) ; } param data param bind param unbind @ Initializable . assert_initialized ( ) data ( data , mipLevel = 0 , bind = true , unbind = true ) { const gl = this . context . gl ; const gl2facade = this . context . gl2facade ; const size = this . calculateMipLevelSize ( mipLevel ) ; let bytesPerFace = size * size * byteSizeOfFormat ( this . context , this . _internalFormat as GLenum ) ; if ( this . _type === this . context . gl2facade . HALF_FLOAT && this . _internalFormat !== this . context . gl . RGBA16F ) { bytesPerFace *= 2 ; } else if ( this . _type === this . context . gl . FLOAT && this . _internalFormat !== this . context . gl . RGBA16F ) { bytesPerFace *= 4 ; } if ( bind ) { this . bind ( ) ; } if ( data instanceof Array && data . length === 2 ) { gl2facade . texImage2D ( data [ 0 ] , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , data [ 1 ] ) ; const id = this . faceID ( data [ 0 ] ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ id ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ id ] = bytesPerFace ; } else { const perFaceData = data as TextureCube . PerFaceData ; if ( perFaceData . positiveX !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_X , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . positiveX ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 0 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 0 ] = bytesPerFace ; } if ( perFaceData . negativeX !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_X , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . negativeX ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 1 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 1 ] = bytesPerFace ; } if ( perFaceData . positiveY !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_Y , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . positiveY ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 2 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 2 ] = bytesPerFace ; } if ( perFaceData . negativeY !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_Y , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . negativeY ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 3 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 3 ] = bytesPerFace ; } if ( perFaceData . positiveZ !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_POSITIVE_Z , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . positiveZ ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 4 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 4 ] = bytesPerFace ; } if ( perFaceData . negativeZ !== undefined ) { gl2facade . texImage2D ( gl . TEXTURE_CUBE_MAP_NEGATIVE_Z , mipLevel , this . _internalFormat , size , size , 0 , this . _format , this . _type , perFaceData . negativeZ ) ; this . context . allocationRegister . deallocate ( this . _identifier , this . _bytes [ 5 ] ) ; this . context . allocationRegister . allocate ( this . _identifier , bytesPerFace ) ; this . _bytes [ 5 ] = bytesPerFace ; } } if ( unbind ) { this . unbind ( ) ; } } param mag param min param bind param unbind @ Initializable . assert_initialized ( ) filter ( mag , min , bind = true , unbind = true ) { const gl = this . context . gl ; if ( bind ) { this . bind ( ) ; } gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_MAG_FILTER , mag ) ; gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_MIN_FILTER , min ) ; if ( unbind ) { this . unbind ( ) ; } } param wrap_s param wrap_t param bind param unbind @ Initializable . assert_initialized ( ) wrap ( wrap_s , wrap_t , bind = true , unbind = true ) { const gl = this . context . gl ; if ( bind ) { this . bind ( ) ; } gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_WRAP_S , wrap_s ) ; gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_WRAP_T , wrap_t ) ; if ( unbind ) { this . unbind ( ) ; } } param baseLevel param maxLevel param bind param unbind @ Initializable . assert_initialized ( ) levels ( baseLevel , maxLevel , bind = true , unbind = true ) { const gl = this . context . gl ; if ( bind ) { this . bind ( ) ; } gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_BASE_LEVEL , baseLevel ) ; gl . texParameteri ( gl . TEXTURE_CUBE_MAP , gl . TEXTURE_MAX_LEVEL , maxLevel ) ; if ( unbind ) { this . unbind ( ) ; } } /**
     * This can be used to reformat the texture image without creating a new texture object. Please note that this
     * resets the texture's image data to undefined. @see  data for setting new image data.
     *  param internalFormat param format param type param bind param unbind @ Initializable . assert_initialized ( ) reformat ( internalFormat , format ? , type ? , bind = true , unbind = true ) { if ( internalFormat === this . _internalFormat && ( format === undefined || format === this . _format ) && ( type === undefined || type === this . _type ) ) { return ; } assert ( internalFormat !== undefined , `valid internal format expected` ) ; this . _internalFormat = internalFormat ; if ( format ) { this . _format = format ; } if ( type ) { this . _type = type ; } this . data ( { clearOnUndefined : true } , 0 , bind , unbind ) ; } param size param bind param unbind @ Initializable . assert_initialized ( ) resize ( size , bind = true , unbind = true ) { if ( size === this . _size ) { return ; } this . _size = size ; this . data ( { clearOnUndefined : true } , 0 , bind , unbind ) ; } /**
     * Returns the number of bytes this object approximately allocates on the GPU. The size will be zero when no
     * image data was passed to the texture object.
     */ get bytes ( ) : GLsizei { this . assertInitialized ( ) ; return this . context . allocationRegister . allocated ( this . _identifier ) ; } /**
     * Cached internal format of the texture for efficient resize. This can only be changed by re-initialization.
     */ get internalFormat ( ) : GLenum { this . assertInitialized ( ) ; return this . _internalFormat as GLenum ; } /**
     * Cached format of the data provided to the texture object for efficient resize. This is set on initialization and
     * might change on data transfers.
     */ get format ( ) : GLenum { this . assertInitialized ( ) ; return this . _format as GLenum ; } /**
     * Cached type of the data provided to the texture used for efficient resize. This is set on initialization and
     * might change on data transfers.
     */ get type ( ) : GLenum { this . assertInitialized ( ) ; return this . _type as GLenum ; } /**
     * The width/height of the texture object (each cube map face is required to be a square).
     */ get size ( ) : GLsizei { this . assertInitialized ( ) ; return this . _size ; } } export namespace TextureCube { export interface PerFaceURI { positiveX ? : string ; negativeX ? : string ; positiveY ? : string ; negativeY ? : string ; positiveZ ? : string ; negativeZ ? : string ; } export interface PerFaceData { positiveX ? : TexImage2DData ; negativeX ? : TexImage2DData ; positiveY ? : TexImage2DData ; negativeY ? : TexImage2DData ; positiveZ ? : TexImage2DData ; negativeZ ? : TexImage2DData ; clearOnUndefined : boolean ; } }