Source: texture/Texture.js

import Class from '../core/Class';
import math from '../math/math';
import extensions from '../renderer/extensions';
import capabilities from '../renderer/capabilities';
import Cache from '../utils/Cache';
import log from '../utils/log';

import constants from '../constants';

const {
    TEXTURE_2D,
    FLOAT,
    RGB,
    RGBA,
    RGB32F,
    RGBA32F,
    LINEAR,
    NEAREST,
    REPEAT,
    CLAMP_TO_EDGE,
    UNSIGNED_BYTE,
    UNPACK_PREMULTIPLY_ALPHA_WEBGL,
    UNPACK_FLIP_Y_WEBGL,
    UNPACK_COLORSPACE_CONVERSION_WEBGL,
    BROWSER_DEFAULT_WEBGL,
    NONE,
} = constants;

const cache = new Cache();
/**
 * 纹理
 * @class
 * @example
 * var loader = new Hilo3d.BasicLoader();
 * loader.load({
 *     src: '//img.alicdn.com/tfs/TB1aNxtQpXXXXX1XVXXXXXXXXXX-1024-1024.jpg',
 *     crossOrigin: true
 * }).then(img => {
 *     return new Hilo3d.Texture({
 *         image: img
 *     });
 * });
 */
const Texture = Class.create(/** @lends Texture.prototype */ {
    Statics: {
        /**
         * 缓存
         * @memberOf Texture
         * @readOnly
         * @type {Object}
         */
        cache: {
            get() {
                return cache;
            }
        },
        /**
         * 重置
         * @memberOf Texture
         * @param  {WebGLRenderingContext} gl
         */
        reset(gl) {
            cache.each((glTexture, id) => {
                gl.deleteTexture(glTexture);
                cache.remove(id);
            });
        }
    },

    /**
     * @default true
     * @type {boolean}
     */
    isTexture: true,

    /**
     * @default Texture
     * @type {string}
     */
    className: 'Texture',

    /**
     * 图片资源是否可以释放,可以的话,上传到GPU后将释放图片引用
     * @type {boolean}
     * @default false
     */
    isImageCanRelease: false,
    _isImageReleased: false,
    _image: null,
    /**
     * 图片对象
     * @type {HTMLImageElement}
     * @default null
     */
    image: {
        get() {
            if (this._isImageReleased) {
                log.errorOnce(`Read Texture.image(${this.id})`, 'Read Texture.image after image released!');
            }
            return this._image;
        },
        set(_img) {
            this._image = _img;
            this._isImageReleased = false;
        }
    },

    _releaseImage() {
        this._canvasImage = null;
        this._canvasCtx = null;
        this._originImage = null;
        this._image = null;
        this.mipmaps = null;
        this._isImageReleased = true;
    },

    /**
     * mipmaps
     * @type {HTMLImageElement[]|TypedArray[]}
     * @default null
     */
    mipmaps: null,

    /**
     * Texture Target
     * @default gl.TEXTURE_2D
     * @type {GLenum}
     */
    target: TEXTURE_2D,

    /**
     * Texture Internal Format
     * @default gl.RGBA
     * @type {GLenum}
     */
    internalFormat: RGBA,

    /**
     * 图片 Format
     * @default gl.RGBA
     * @type {GLenum}
     */
    format: RGBA,

    /**
     * 类型
     * @default gl.UNSIGNED_BYTE
     * @type {GLenum}
     */
    type: UNSIGNED_BYTE,

    /**
     * @default 0
     * @type {number}
     */
    width: 0,

    /**
     * @default 0
     * @type {number}
     */
    height: 0,

    /**
     * @default 0
     * @readOnly
     * @type {Number}
     */
    border: 0,

    /**
     * magFilter
     * @default gl.LINEAR
     * @type {GLenum}
     */
    magFilter: LINEAR,

    /**
     * minFilter
     * @default gl.LINEAR
     * @type {GLenum}
     */
    minFilter: LINEAR,

    /**
     * wrapS
     * @default gl.REPEAT
     * @type {GLenum}
     */
    wrapS: REPEAT,

    /**
     * wrapT
     * @default gl.REPEAT
     * @type {GLenum}
     */
    wrapT: REPEAT,

    /**
     * @type {string}
     */
    name: '',

    /**
     * @default false
     * @type {boolean}
     */
    premultiplyAlpha: false,

    /**
     * 是否翻转Texture的Y轴
     * @default false
     * @type {boolean}
     */
    flipY: false,

    /**
     * 是否转换到图片默认的颜色空间
     * @default true
     * @type {boolean}
     */
    colorSpaceConversion: true,

    /**
     * 是否压缩
     * @default false
     * @type {Boolean}
     */
    compressed: false,

    /**
     * 是否需要更新Texture
     * @default true
     * @type {boolean}
     */
    needUpdate: true,
    /**
     * 是否需要销毁之前的Texture,Texture参数变更之后需要销毁
     * @default false
     * @type {boolean}
     */
    needDestroy: false,

    /**
     * 是否每次都更新Texture
     * @default false
     * @type {boolean}
     */
    autoUpdate: false,
    /**
     * uv
     * @default 0
     * @type {Number}
     */
    uv: 0,

    /**
     * anisotropic
     * @default 1
     * @type {Number}
     */
    anisotropic: 1,

    /**
     * 获取原始图像宽度。
     * @default 0
     * @type {Number}
     */
    origWidth: {
        get() {
            if (this._originImage) {
                return this._originImage.width || this.width;
            }

            if (this.image) {
                return this.image.width || this.width;
            }

            return this.width;
        }
    },

    /**
     * 获取原始图像高度。
     * @default 0
     * @type {Number}
     */
    origHeight: {
        get() {
            if (this.originImage) {
                return this._originImage.height || this.height;
            }

            if (this.image) {
                return this.image.height || this.height;
            }

            return this.height;
        }
    },

    /**
     * 是否使用 mipmap
     * @readOnly
     * @type {Boolean}
     */
    useMipmap: {
        get() {
            return this.minFilter !== LINEAR && this.minFilter !== NEAREST;
        },
        set() {
            log.warn('texture.useMipmap is readOnly!');
        }
    },

    /**
     * 是否使用 repeat
     * @readOnly
     * @type {Boolean}
     */
    useRepeat: {
        get() {
            return this.wrapS !== CLAMP_TO_EDGE || this.wrapT !== CLAMP_TO_EDGE;
        },
        set() {
            log.warn('texture.useRepeat is readOnly!');
        }
    },

    /**
     * mipmapCount
     * @readOnly
     * @type {Number}
     */
    mipmapCount: {
        get() {
            return Math.floor(Math.log2(Math.max(this.width, this.height)) + 1);
        },
        set() {
            log.warn('texture.mipmapCount is readOnly!');
        }
    },

    /**
     * @constructs
     * @param {object} [params] 初始化参数,所有params都会复制到实例上
     */
    constructor(params) {
        this.id = math.generateUUID(this.className);
        Object.assign(this, params);
    },
    /**
     * 是否是 2 的 n 次方
     * @param  {HTMLImageElement}  img
     * @return {Boolean}
     */
    isImgPowerOfTwo(img) {
        return math.isPowerOfTwo(img.width) && math.isPowerOfTwo(img.height);
    },
    /**
     * 获取支持的尺寸
     * @param  {HTMLImageElement} img
     * @param  {Boolean} [needPowerOfTwo=false]
     * @return {Object} { width, height }
     */
    getSupportSize(img, needPowerOfTwo = false) {
        let width = img.width;
        let height = img.height;

        if (needPowerOfTwo && !this.isImgPowerOfTwo(img)) {
            width = math.nextPowerOfTwo(width);
            height = math.nextPowerOfTwo(height);
        }

        const maxTextureSize = capabilities.MAX_TEXTURE_SIZE;
        if (maxTextureSize) {
            if (width > maxTextureSize) {
                width = maxTextureSize;
            }

            if (height > maxTextureSize) {
                height = maxTextureSize;
            }
        }

        return {
            width,
            height
        };
    },
    /**
     * 更新图片大小成为 2 的 n 次方
     * @param  {HTMLImageElement} img
     * @return {HTMLCanvasElement|HTMLImageElement}
     */
    resizeImgToPowerOfTwo(img) {
        const sizeResult = this.getSupportSize(img, true);
        return this.resizeImg(img, sizeResult.width, sizeResult.height);
    },
    /**
     * 更新图片大小
     * @param  {HTMLImageElement} img
     * @param {Number} width
     * @param {Number} height
     * @return {HTMLCanvasElement|HTMLImageElement}
     */
    resizeImg(img, width, height) {
        if (img.width === width && img.height === height) {
            return img;
        }

        let canvas = this._canvasImage;
        if (!canvas) {
            canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            this._canvasImage = canvas;
            this._canvasCtx = canvas.getContext('2d');
        } else {
            canvas.width = width;
            canvas.height = height;
            this._canvasCtx = canvas.getContext('2d');
        }
        this._canvasCtx.drawImage(img, 0, 0, img.width, img.height, 0, 0, width, height);
        log.warnOnce(`Texture.resizeImg(${this.id})`, `image size(${img.width}x${img.height}) is not support. Resized to ${canvas.width}x${canvas.height}`, img.src);
        this._originImage = img;
        return canvas;
    },
    /**
     * GL上传贴图
     * @private
     * @param  {WebGLState} state
     * @param  {GLenum} target
     * @param  {HTMLImageElement|TypedArray} image
     * @param  {HTMLImageElement} [level=0]
     * @param  {Number} [width=this.width]
     * @param  {Number} [height=this.height]
     * @return {Texture}  this
     */
    _glUploadTexture(state, target, image, level = 0, width = this.width, height = this.height) {
        const gl = state.gl;
        const type = this.type;
        const format = this.format;
        let internalFormat = this.internalFormat;

        if (this.compressed) {
            gl.compressedTexImage2D(target, level, internalFormat, width, height, this.border, image);
        } else {
            internalFormat = this._fixInternalFormat(state, type, format, internalFormat);
            if (image && image.width !== undefined) {
                gl.texImage2D(target, level, internalFormat, format, this.type, image);
            } else {
                gl.texImage2D(target, level, internalFormat, width, height, this.border, format, this.type, image);
            }
        }

        return this;
    },
    /**
     * 修复 WebGL & WebGL2 internalFormat
     * @param {WebGLState} state
     * @returns {number} internalFormat
     */
    _fixInternalFormat(state, type, format, internalFormat) {
        if (state.isWebGL2) {
            if (type === FLOAT) {
                if (format === RGBA) {
                    internalFormat = RGBA32F;
                } else if (format === RGB) {
                    internalFormat = RGB32F;
                }
            }
        } else if (format !== internalFormat) {
            internalFormat = this.format;
        }
        return internalFormat;
    },
    /**
     * 上传贴图,子类可重写
     * @private
     * @param  {WebGLState} state
     * @return {Texture} this
     */
    _uploadTexture(state) {
        if (this.useMipmap && this.mipmaps) {
            this.mipmaps.forEach((mipmap, index) => {
                this._glUploadTexture(state, this.target, mipmap.data, index, mipmap.width, mipmap.height);
            });
        } else {
            this._glUploadTexture(state, this.target, this.image, 0);
        }

        return this;
    },

    _updatePixelStorei() {
        const state = this.state;
        state.pixelStorei(UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);
        state.pixelStorei(UNPACK_FLIP_Y_WEBGL, !!this.flipY);
        state.pixelStorei(UNPACK_COLORSPACE_CONVERSION_WEBGL, this.colorSpaceConversion ? BROWSER_DEFAULT_WEBGL : NONE);
    },

    /**
     * 更新 Texture
     * @param  {WebGLState} state
     * @param  {WebGLTexture} glTexture
     * @return {Texture} this
     */
    updateTexture(state, glTexture) {
        const gl = state.gl;
        if (this.needUpdate || this.autoUpdate) {
            if (this._originImage && this.image === this._canvasImage) {
                this.image = this._originImage;
            }
            const useMipmap = this.useMipmap;
            const useRepeat = this.useRepeat;

            if (this.image && !this.image.length) {
                if (!state.isWebGL2) {
                    const needPowerOfTwo = useRepeat || useMipmap;
                    const sizeResult = this.getSupportSize(this.image, needPowerOfTwo);
                    this.image = this.resizeImg(this.image, sizeResult.width, sizeResult.height);
                }
                this.width = this.image.width;
                this.height = this.image.height;
            }

            state.activeTexture(gl.TEXTURE0 + capabilities.MAX_TEXTURE_INDEX);
            state.bindTexture(this.target, glTexture);
            this._updatePixelStorei();
            this._uploadTexture(state);
            if (useMipmap) {
                if (!this.compressed) {
                    gl.generateMipmap(this.target);
                } else if (!this.mipmaps) {
                    log.warn(`Compressed texture has no mipmips, changed the minFilter from ${this.minFilter} to Linear!`, this);
                    this.minFilter = LINEAR;
                }
            }

            gl.texParameterf(this.target, gl.TEXTURE_MAG_FILTER, this.magFilter);
            gl.texParameterf(this.target, gl.TEXTURE_MIN_FILTER, this.minFilter);
            gl.texParameterf(this.target, gl.TEXTURE_WRAP_S, this.wrapS);
            gl.texParameterf(this.target, gl.TEXTURE_WRAP_T, this.wrapT);

            const textureFilterAnisotropic = extensions.textureFilterAnisotropic;
            if (textureFilterAnisotropic && this.anisotropic > 1) {
                gl.texParameterf(this.target, textureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(this.anisotropic, capabilities.MAX_TEXTURE_MAX_ANISOTROPY));
            }

            this.needUpdate = false;
        }

        if (this._needUpdateSubTexture) {
            this._uploadSubTextures(state, glTexture);
            this._needUpdateSubTexture = false;
        }

        return this;
    },
    /**
     * 跟新所有的局部贴图
     * @private
     * @param  {WebGLState} state
     * @param  {WebGLTexture} glTexture
     */
    _uploadSubTextures(state, glTexture) {
        if (this._subTextureList && this._subTextureList.length > 0) {
            const gl = state.gl;
            state.activeTexture(gl.TEXTURE0 + capabilities.MAX_TEXTURE_INDEX);
            state.bindTexture(this.target, glTexture);
            this._updatePixelStorei();

            this._subTextureList.forEach((subInfo) => {
                const xOffset = subInfo[0];
                const yOffset = subInfo[1];
                const image = subInfo[2];

                gl.texSubImage2D(this.target, 0, xOffset, yOffset, this.format, this.type, image);
            });
            this._subTextureList.length = 0;
        }
    },
    _needUpdateSubTexture: false,
    _subTextureList: null,
    /**
     * 跟新局部贴图
     * @param  {Number} xOffset
     * @param  {Number} yOffset
     * @param  {HTMLImageElement|HTMLCanvasElement|ImageData} image
     */
    updateSubTexture(xOffset, yOffset, image) {
        if (!this._subTextureList) {
            this._subTextureList = [];
        }
        this._subTextureList.push([xOffset, yOffset, image]);
        this._needUpdateSubTexture = true;
    },
    /**
     * 获取 GLTexture
     * @param  {WebGLState} state
     * @return {WebGLTexture}
     */
    getGLTexture(state) {
        this.state = state;
        const gl = this.gl = state.gl;
        const id = this.id;

        if (this.needDestroy) {
            this.destroy();
            this.needDestroy = false;
        }

        let glTexture = cache.get(id);
        if (glTexture) {
            this.updateTexture(state, glTexture);
        } else {
            glTexture = gl.createTexture();
            cache.add(id, glTexture);
            this.needUpdate = true;
            this.updateTexture(state, glTexture);
        }

        if (this.isImageCanRelease) {
            this._releaseImage();
        }

        return glTexture;
    },
    /**
     * 设置 GLTexture
     * @param {WebGLTexture}  texture
     * @param {Boolean} [needDestroy=false] 是否销毁之前的 GLTexture
     * @return {Texture} this
     */
    setGLTexture(texture, needDestroy = false) {
        if (needDestroy) {
            this.destroy();
        }
        cache.add(this.id, texture);

        return this;
    },
    /**
     * 销毁当前Texture
     * @return {Texture} this
     */
    destroy() {
        const id = this.id;
        const glTexture = cache.get(id);
        if (glTexture && this.gl) {
            this.gl.deleteTexture(glTexture);
            cache.remove(id);
        }
        return this;
    },
    /**
     * clone
     * @return {Texture}
     */
    clone() {
        const option = Object.assign({}, this);
        delete option.id;
        const texture = new this.constructor(option);
        return texture;
    }
});

export default Texture;