Source: renderer/Framebuffer.js

import Class from '../core/Class';
import Shader from '../shader/Shader';
import screenVert from '../shader/screen.vert';
import screenFrag from '../shader/screen.frag';
import Cache from '../utils/Cache';
import log from '../utils/log';
import Program from './Program';
import VertexArrayObject from './VertexArrayObject';
import math from '../math/math';
import Color from '../math/Color';
import GeometryData from '../geometry/GeometryData';
import Texture from '../texture/Texture';
import {
    getTypedArrayClass
} from '../utils/util';

import constants from '../constants';
import extensions from './extensions';
import capabilities from './capabilities';

const {
    FRAMEBUFFER,
    TEXTURE_2D,
    RGBA,
    UNSIGNED_BYTE,
    COLOR_ATTACHMENT0,
    DEPTH_STENCIL_ATTACHMENT,
    DEPTH_STENCIL,
    DEPTH_TEST,
    CULL_FACE,
    TRIANGLE_STRIP,
    NEAREST,
    CLAMP_TO_EDGE,
    COLOR_BUFFER_BIT,
    READ_FRAMEBUFFER,
    DRAW_FRAMEBUFFER,
} = constants;

const cache = new Cache();

const defaultAttachmentOptions = {
    framebufferTarget: FRAMEBUFFER,
    attachment: COLOR_ATTACHMENT0,
    target: TEXTURE_2D,
    format: RGBA,
    internalFormat: RGBA,
    type: UNSIGNED_BYTE,
    minFilter: NEAREST,
    magFilter: NEAREST,
    wrapS: CLAMP_TO_EDGE,
    wrapT: CLAMP_TO_EDGE,
};

/**
 * 帧缓冲
 * @class
 */
const Framebuffer = Class.create(/** @lends Framebuffer.prototype */ {
    Statics: {
        ATTACHMENT_TYPE_TEXTURE: 'TEXTURE',
        ATTACHMENT_TYPE_RENDERBUFFER: 'RENDERBUFFER',
        /**
         * 缓存
         * @readOnly
         * @memberOf Framebuffer
         * @type {Cache}
         */
        cache: {
            get() {
                return cache;
            }
        },
        /**
         * 重置所有framebuffer
         * @memberOf Framebuffer
         * @param  {WebGLRenderingContext} gl
         */
        reset(gl) { // eslint-disable-line no-unused-vars
            cache.each((framebuffer) => {
                framebuffer.reset();
            });
        },
        /**
         * 销毁所有 Framebuffer
         * @memberOf Framebuffer
         * @param  {WebGLRenderingContext} gl
         */
        destroy(gl) { // eslint-disable-line no-unused-vars
            cache.each((framebuffer) => {
                framebuffer.destroy();
            });
        },
    },

    /**
     * @default Framebuffer
     * @type {String}
     */
    className: 'Framebuffer',

    /**
     * @default true
     * @type {Boolean}
     */
    isFramebuffer: true,

    /**
     * bufferInternalFormat
     * @type {GLenum}
     * @default gl.DEPTH_STENCIL
     */
    bufferInternalFormat: DEPTH_STENCIL,

    /**
     * framebufferTarget
     * @type {GLenum}
     * @default gl.FRAMEBUFFER
     */
    framebufferTarget: defaultAttachmentOptions.framebufferTarget,

    /**
     * texture target
     * @type {GLenum}
     * @default gl.TEXTURE_2D
     */
    target: defaultAttachmentOptions.target,

    /**
     * texture format
     * @type {GLenum}
     * @default gl.RGBA
     */
    format: defaultAttachmentOptions.format,

    /**
     * texture internalFormat
     * @type {GLenum}
     * @default gl.RGBA
     */
    internalFormat: defaultAttachmentOptions.internalFormat,

    /**
     * texture type
     * @type {GLenum}
     * @default gl.UNSIGNED_BYTE
     */
    type: defaultAttachmentOptions.type,
    /**
     * texture minFilter
     * @type {GLenum}
     * @default gl.NEAREST
     */
    minFilter: defaultAttachmentOptions.minFilter,

    /**
     * texture magFilter
     * @type {GLenum}
     * @default gl.NEAREST
     */
    magFilter: defaultAttachmentOptions.magFilter,

    /**
     * texture wrapS
     * @type {GLenum}
     * @default gl.CLAMP_TO_EDGE
     */
    wrapS: defaultAttachmentOptions.wrapS,

    /**
     * texture wrapS
     * @type {GLenum}
     * @default gl.CLAMP_TO_EDGE
     */
    wrapT: defaultAttachmentOptions.wrapT,

    /**
     * texture data
     * @type {TypedArray}
     * @default null
     */
    data: null,

    /**
     * attachment
     * @type {GLenum}
     * @default gl.COLOR_ATTACHMENT0
     */
    attachment: defaultAttachmentOptions.attachment,

    /**
     * 是否需要renderbuffer
     * @type {Boolean}
     * @default true
     */
    needRenderbuffer: true,

    /**
     * 是否使用VAO
     * @type {Boolean}
     * @default true
     */
    useVao: true,

    /**
     * renderer
     * @type {WebGLRenderer}
     * @default null
     */
    renderer: null,

    /**
     * texture
     * @type {Texture}
     */
    texture: null,

    /**
     * renderbuffer
     * @type {WebGLRenderbuffer}
     */
    renderbuffer: null,

    /**
     * framebuffer
     * @type {WebGLFramebuffer}
     */
    framebuffer: null,

    _isInit: false,

    /**
     * colorAttachmentInfos
     * @type {AttachmentInfo[]}
     */
    colorAttachmentInfos: undefined,

    /**
     * depthStencilAttachmentInfo
     * @type {AttachmentInfo}
     */
    depthStencilAttachmentInfo: undefined,


    /**
     * @constructs
     * @param {WebGLRenderer}  renderer
     * @param  {Object} [params] 初始化参数,所有params都会复制到实例上
     */
    constructor(renderer, params) {
        this.id = math.generateUUID(this.className);
        this.renderer = renderer;
        Object.assign(this, params);

        if (!this.width) {
            this.width = renderer.width;
        }

        if (!this.height) {
            this.height = renderer.height;
        }

        if (this.colorAttachmentInfos === undefined) {
            this.colorAttachmentInfos = [{
                attachmentType: Framebuffer.ATTACHMENT_TYPE_TEXTURE,
                framebufferTarget: this.framebufferTarget,
                target: this.target,
                format: this.format,
                internalFormat: this.internalFormat,
                type: this.type,
                minFilter: this.minFilter,
                magFilter: this.magFilter,
                wrapS: this.wrapS,
                wrapT: this.wrapT,
                data: this.data,
            }];
        }

        if (this.depthStencilAttachmentInfo === undefined && this.needRenderbuffer) {
            this.depthStencilAttachmentInfo = {
                attachmentType: Framebuffer.ATTACHMENT_TYPE_RENDERBUFFER,
                framebufferTarget: this.framebufferTarget,
                attachment: DEPTH_STENCIL_ATTACHMENT,
                internalFormat: DEPTH_STENCIL,
            };
        }

        cache.add(this.id, this);
    },
    /**
     * init
     */
    init() {
        if (!this._isInit && this.renderer.isInit) {
            this._isInit = true;
            const renderer = this.renderer;
            this.gl = renderer.gl;
            this.state = renderer.state;
            this.reset();
        }
    },
    /**
     * reset
     * @private
     */
    reset() {
        this.destroyResource();
        const gl = this.gl;
        /**
         * framebuffer
         * @type {WebGLFramebuffer}
         */
        this.framebuffer = gl.createFramebuffer();
        this.bind();

        this._createAttachments();

        if (!this.isComplete()) {
            log.warn('Framebuffer is not complete => ' + gl.checkFramebufferStatus(gl.FRAMEBUFFER));
        }

        this.unbind();
    },
    _createAttachments() {
        const { colorAttachmentInfos, depthStencilAttachmentInfo } = this;
        const drawBuffers = [];
        if (colorAttachmentInfos) {
            colorAttachmentInfos.forEach((attachmentInfo, index) => {
                const attachment = COLOR_ATTACHMENT0 + index;
                switch (attachmentInfo.attachmentType) {
                    case Framebuffer.ATTACHMENT_TYPE_RENDERBUFFER:
                        this._createRenderbufferAttachment(attachmentInfo, attachment);
                        break;
                    case Framebuffer.ATTACHMENT_TYPE_TEXTURE:
                    default:
                        this._createTextureAttachment(attachmentInfo, attachment);
                        break;
                }

                drawBuffers.push(attachment);
            });
        }

        if (depthStencilAttachmentInfo) {
            const attachment = depthStencilAttachmentInfo.attachment;
            switch (depthStencilAttachmentInfo.attachmentType) {
                case Framebuffer.ATTACHMENT_TYPE_RENDERBUFFER:
                    this._createRenderbufferAttachment(depthStencilAttachmentInfo, attachment);
                    break;
                case Framebuffer.ATTACHMENT_TYPE_TEXTURE:
                default:
                    this._createTextureAttachment(depthStencilAttachmentInfo, attachment);
                    break;
            }
        }

        if (drawBuffers.length > 1 && capabilities.DRAW_BUFFERS) {
            extensions.drawBuffers.drawBuffers(drawBuffers);
        }
    },
    _createTextureAttachment(attachmentInfo, attachment) {
        const state = this.state;
        const gl = state.gl;

        const textureOptions = Object.assign({}, defaultAttachmentOptions, attachmentInfo);
        const texture = new Texture({
            minFilter: textureOptions.minFilter,
            magFilter: textureOptions.magFilter,
            internalFormat: textureOptions.internalFormat,
            format: textureOptions.format,
            type: textureOptions.type,
            width: this.width,
            height: this.height,
            image: textureOptions.data,
            wrapS: textureOptions.wrapS,
            wrapT: textureOptions.wrapT
        });

        const glTexture = texture.getGLTexture(state);
        gl.framebufferTexture2D(textureOptions.framebufferTarget, attachment, textureOptions.target, glTexture, 0);
        attachmentInfo.texture = texture;

        if (attachment === COLOR_ATTACHMENT0) {
            this.texture = texture;
        }
        return texture;
    },
    _createRenderbufferAttachment(attachmentInfo, attachment) {
        const {
            gl,
            width,
            height
        } = this;
        const renderbuffer = gl.createRenderbuffer();
        gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
        if (attachmentInfo.samples > 0) {
            gl.renderbufferStorageMultisample(gl.RENDERBUFFER, attachmentInfo.samples, attachmentInfo.internalFormat, width, height);
        } else {
            gl.renderbufferStorage(gl.RENDERBUFFER, attachmentInfo.internalFormat, width, height);
        }
        gl.framebufferRenderbuffer(attachmentInfo.framebufferTarget || defaultAttachmentOptions.framebufferTarget, attachment, gl.RENDERBUFFER, renderbuffer);
        attachmentInfo.renderbuffer = renderbuffer;

        return renderbuffer;
    },
    /**
     * framebuffer 是否完成
     * @return {Boolean}
     */
    isComplete() {
        const gl = this.gl;
        if (gl && gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
            return true;
        }
        return false;
    },
    /**
     * 绑定
     */
    bind() {
        this.init();
        if (this._isInit) {
            this._preFramebuffer = this.state.currentFramebuffer;
            this.state.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
        }
    },
    /**
     * 解绑
     */
    unbind() {
        this.init();
        if (this._isInit) {
            const state = this.state;
            state.bindFramebuffer(this.gl.FRAMEBUFFER, this._preFramebuffer);
        }
    },
    clear(clearColor = new Color(0, 0, 0, 0)) {
        if (this._isInit) {
            const {
                gl
            } = this;
            gl.clearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        }
    },
    /**
     * 渲染当前纹理
     * @param  {Number} [x=0]
     * @param  {Number} [y=0]
     * @param  {Number} [width=1]
     * @param  {Number} [height=1]
     * @param  {Color} [clearColor=null]
     */
    render(x = 0, y = 0, width = 1, height = 1, clearColor = null, texture = null) {
        if (this._isInit) {
            const {
                gl,
                state,
                colorAttachmentInfos,
            } = this;

            if (!texture) {
                if (colorAttachmentInfos[0]) {
                    texture = colorAttachmentInfos[0].texture;
                } else {
                    return;
                }
            }

            state.disable(DEPTH_TEST);
            state.disable(CULL_FACE);

            if (clearColor) {
                this.clear(clearColor);
            }

            const shader = Shader.getCustomShader(screenVert, screenFrag, '', 'FramebufferTextureShader');
            const program = Program.getProgram(shader, state);
            program.useProgram();

            const vaoId = `${x}_${y}_${width}_${height}_${program.id}`;
            const vao = VertexArrayObject.getVao(gl, vaoId, {
                useVao: this.useVao,
                useInstanced: false,
                mode: TRIANGLE_STRIP
            });

            if (vao.isDirty) {
                vao.isDirty = false;
                x = x * 2 - 1;
                y = 1 - y * 2;
                width *= 2;
                height *= 2;
                const vertices = [x, y, x + width, y, x, y - height, x + width, y - height];
                vao.addAttribute(new GeometryData(new Float32Array(vertices), 2), program.attributes.a_position);
                vao.addAttribute(new GeometryData(new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]), 2), program.attributes.a_texcoord0);
            }

            state.activeTexture(gl.TEXTURE0);
            state.bindTexture(gl.TEXTURE_2D, texture.getGLTexture(state));
            vao.draw();
        }
    },
    /**
     * resize
     * @param  {Number} width
     * @param  {Number} height
     * @param  {Boolean} [force=true]
     */
    resize(width, height, force) {
        if (force || this.width !== width || this.height !== height) {
            this.width = width;
            this.height = height;

            if (this._isInit) {
                this.reset();
            }
        }
    },
    /**
     * 读取区域像素
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} [width=1]
     * @param  {Number} [height=1]
     * @return {TypedArray}
     */
    readPixels(x, y, width = 1, height = 1) {
        const TypedArray = getTypedArrayClass(this.type);
        const pixels = new TypedArray(width * height * 4);

        if (this._isInit) {
            const gl = this.gl;
            // convert to webgl coordinate system
            y = this.height - y - height;

            this.bind();
            gl.readPixels(x, y, width, height, this.format, this.type, pixels);
            this.unbind();
        }

        return pixels;
    },
    /**
     * copy framebuffer
     */
    copyFramebuffer(srcFramebuffer, config = {}) {
        this.init();
        if (this._isInit) {
            const gl = this.gl;
            let {
                mask,
                filter,
                srcSize,
                dstSize
            } = config;

            if (!mask) {
                mask = COLOR_BUFFER_BIT;
            }

            if (!filter) {
                filter = NEAREST;
            }

            if (!srcSize) {
                srcSize = [0, 0, srcFramebuffer.width, srcFramebuffer.height];
            }

            if (!dstSize) {
                dstSize = [0, 0, this.width, this.height];
            }

            gl.bindFramebuffer(READ_FRAMEBUFFER, srcFramebuffer.framebuffer);
            gl.bindFramebuffer(DRAW_FRAMEBUFFER, this.framebuffer);
            gl.blitFramebuffer(
                srcSize[0], srcSize[1], srcSize[2], srcSize[3],
                dstSize[0], dstSize[1], dstSize[2], dstSize[3],
                mask, filter,
            );
            gl.bindFramebuffer(READ_FRAMEBUFFER, null);
            gl.bindFramebuffer(DRAW_FRAMEBUFFER, null);
        }
    },
    /**
     * 销毁资源
     * @return {Framebuffer} this
     */
    destroy() {
        if (this._isDestroyed) {
            return this;
        }
        this.destroyResource();
        this.gl = null;
        cache.removeObject(this);

        this._isDestroyed = true;
        return this;
    },
    /**
     * 只销毁 gl 资源
     * @return {Framebuffer} this
     */
    destroyResource() {
        const gl = this.gl;
        if (gl) {
            if (this.framebuffer) {
                gl.deleteFramebuffer(this.framebuffer);
                this.framebuffer = null;
            }

            if (this.colorAttachmentInfos) {
                this.colorAttachmentInfos.forEach((attachmentInfo) => {
                    const { texture, renderbuffer } = attachmentInfo;
                    attachmentInfo.texture = null;
                    attachmentInfo.renderbuffer = null;
                    if (texture) {
                        texture.destroy();
                    } else if (renderbuffer) {
                        gl.deleteRenderbuffer(renderbuffer);
                    }
                });
            }

            if (this.depthStencilAttachmentInfo) {
                const { texture, renderbuffer } = this.depthStencilAttachmentInfo;
                this.depthStencilAttachmentInfo.texture = null;
                this.depthStencilAttachmentInfo.renderbuffer = null;
                if (texture) {
                    texture.destroy();
                } else if (renderbuffer) {
                    gl.deleteRenderbuffer(renderbuffer);
                }
            }
        }
    }
});

export default Framebuffer;

/**
 * @typedef {object} AttachmentInfo
 * @property {'TEXTURE'|'RENDERBUFFER'} attachmentType
 * @property {GLenum} framebufferTarget
 * @property {GLenum} attachment
 * @property {number} samples
 * @property {GLenum} target
 * @property {GLenum} internalFormat
 * @property {GLenum} format
 * @property {GLenum} type
 * @property {GLenum} minFilter
 * @property {GLenum} magFilter
 * @property {GLenum} wrapS
 * @property {GLenum} wrapT
 * @property {TypedArray} data
 * @property {Texture} texture
 * @property {WebGLRenderbuffer} renderbuffer
 */