Source: renderer/VertexArrayObject.js

import Class from '../core/Class';
import extensions from './extensions';
import Buffer from './Buffer';
import GeometryData from '../geometry/GeometryData';
import bufferUtil from '../utils/bufferUtil';
import Cache from '../utils/Cache';
import log from '../utils/log';
import constants from '../constants';

const {
    TRIANGLES
} = constants;

let globalStates = [];
let currentVao = null;
const cache = new Cache();

/**
 * VAO
 * @class
 */
const VertexArrayObject = Class.create(/** @lends VertexArrayObject.prototype */ {
    Statics: {
        /**
         * 缓存
         * @type {Cache}
         * @readOnly
         * @memberOf VertexArrayObject
         * @return {Cache}
         */
        cache: {
            get() {
                return cache;
            }
        },
        /**
         * 获取 vao
         * @memberOf VertexArrayObject
         * @param  {WebGLRenderingContext} gl
         * @param  {String} id  缓存id
         * @param  {Object} params
         * @return {VertexArrayObject}
         */
        getVao(gl, id, params) {
            let vao = cache.get(id);
            if (!vao) {
                vao = new VertexArrayObject(gl, id, params);
                cache.add(id, vao);
            } else if (params.mode && params.mode !== vao.mode) {
                // for geometry.mode change
                vao.mode = params.mode;
            }

            return vao;
        },
        /**
         * 重置所有vao
         * @memberOf VertexArrayObject
         * @param  {WebGLRenderingContext} gl
         */
        reset(gl) { // eslint-disable-line no-unused-vars
            currentVao = null;
            globalStates = [];
            this.bindSystemVao();
            cache.each((vao) => {
                vao.destroy(gl);
            });
        },
        /**
         * 绑定系统vao
         * @memberOf VertexArrayObject
         */
        bindSystemVao() {
            if (extensions.vao) {
                extensions.vao.bindVertexArray(null);
            }

            currentVao = null;
        }
    },

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

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

    /**
     * 顶点数量
     * @type {Number}
     * @private
     */
    vertexCount: null,

    /**
     * 是否使用 vao
     * @type {Boolean}
     * @default false
     */
    useVao: false,

    /**
     * 是否使用 instanced
     * @type {Boolean}
     * @default false
     */
    useInstanced: false,

    /**
     * 绘图方式
     * @type {GLenum}
     * @default gl.TRIANGLES
     */
    mode: TRIANGLES,

    /**
     * 是否脏
     * @type {Boolean}
     * @default true
     */
    isDirty: true,

    /**
     * @constructs
     * @param  {WebGLRenderingContext} gl
     * @param  {String} id  缓存id
     * @param  {Object} params
     */
    constructor(gl, id, params) {
        this.gl = gl;
        this.id = id;

        this.instancedExtension = extensions.instanced;
        this.vaoExtension = extensions.vao;

        Object.assign(this, params);

        if (!this.vaoExtension) {
            this.useVao = false;
        }

        if (!this.instancedExtension) {
            this.useInstanced = false;
        }

        if (this.useVao) {
            this.vao = this.vaoExtension.createVertexArray();
        }

        this.attributes = [];
        this.activeStates = [];
        this.indexBuffer = null;
    },
    /**
     * bind
     */
    bind() {
        if (currentVao !== this) {
            if (this.useVao) {
                this.vaoExtension.bindVertexArray(this.vao);
            } else {
                this.bindSystemVao();
            }
            currentVao = this;
        }
    },
    /**
     * @private
     */
    bindSystemVao() {
        const gl = this.gl;
        if (currentVao && currentVao.useVao) {
            currentVao.unbind();
        }
        const activeStates = this.activeStates;

        let lastBuffer;
        this.attributes.forEach((attributeObject) => {
            const {
                buffer,
                attribute,
                geometryData
            } = attributeObject;

            if (lastBuffer !== buffer) {
                lastBuffer = buffer;
                buffer.bind();
            }

            attribute.enable();
            attribute.pointer(geometryData);
            if (attributeObject.useInstanced) {
                attribute.divisor(1);
            } else {
                attribute.divisor(0);
            }
        });

        globalStates.forEach((globalAttributeObject, i) => {
            const activeAttributeObject = activeStates[i];
            if (globalAttributeObject && !activeAttributeObject) {
                globalAttributeObject.attribute.divisor(0);
                gl.disableVertexAttribArray(i);
            }
        });

        if (this.indexBuffer) {
            this.indexBuffer.bind();
        }
        globalStates = activeStates;
    },
    /**
     * unbind
     */
    unbind() {
        if (this.useVao) {
            this.vaoExtension.bindVertexArray(null);
        }
        currentVao = null;
    },
    /**
     * draw
     */
    draw() {
        this.bind();
        const {
            gl,
            mode
        } = this;

        if (this.indexBuffer) {
            gl.drawElements(mode, this.vertexCount, this.indexType, 0);
        } else {
            gl.drawArrays(mode, 0, this.getVertexCount());
        }
    },
    /**
     * 获取顶点数量
     * @return {Number} 顶点数量
     */
    getVertexCount() {
        if (this.vertexCount === null) {
            const attributeObj = this.attributes[0];
            if (attributeObj) {
                this.vertexCount = attributeObj.geometryData.count;
            } else {
                this.vertexCount = 0;
            }
        }
        return this.vertexCount;
    },
    /**
     * drawInstance
     * @param  {Number} [primcount=1]
     */
    drawInstance(primcount = 1) {
        this.bind();
        const {
            gl,
            mode
        } = this;
        if (this.useInstanced) {
            if (this.indexBuffer) {
                this.instancedExtension.drawElementsInstanced(mode, this.vertexCount, gl.UNSIGNED_SHORT, 0, primcount);
            } else {
                this.instancedExtension.drawArraysInstanced(mode, 0, this.getVertexCount(), primcount);
            }
        }
    },
    /**
     * addIndexBuffer
     * @param {GeometryData} data
     * @param {GLenum} usage gl.STATIC_DRAW|gl.DYNAMIC_DRAW
     * @return {Buffer} Buffer
     */
    addIndexBuffer(geometryData, usage) {
        this.bind();
        const gl = this.gl;
        let buffer = this.indexBuffer;
        this.indexType = geometryData.type;
        if (!buffer) {
            buffer = Buffer.createIndexBuffer(gl, geometryData, usage);
            buffer.bind();
            this.indexBuffer = buffer;
            this.vertexCount = geometryData.length;
        } else if (geometryData.isDirty) {
            buffer.uploadGeometryData(geometryData);
            this.vertexCount = geometryData.length;
        }

        return buffer;
    },
    /**
     * addAttribute
     * @param {GeometryData} geometryData
     * @param {Object} attribute
     * @param {GLenum} usage gl.STATIC_DRAW|gl.DYNAMIC_DRAW
     * @param {Function} onInit
     * @return {AttributeObject} attributeObject
     */
    addAttribute(geometryData, attribute, usage, onInit) {
        this.bind();
        const gl = this.gl;
        const name = attribute.name;

        let attributeObject = this[name];
        if (!attributeObject) {
            const buffer = Buffer.createVertexBuffer(gl, geometryData, usage);
            buffer.bind();
            attribute.enable();
            attribute.pointer(geometryData);
            attributeObject = {
                attribute,
                buffer,
                geometryData
            };
            this.attributes.push(attributeObject);
            this[name] = attributeObject;
            attribute.addTo(this.activeStates, attributeObject);
            if (onInit) {
                onInit(attributeObject);
            }
        }

        if (geometryData.isDirty) {
            const buffer = attributeObject.buffer;
            buffer.bind();
            attribute.enable();
            attribute.pointer(geometryData);
            buffer.uploadGeometryData(geometryData);
        }

        return attributeObject;
    },
    /**
     * addInstancedAttribute
     * @param {Object} attribute
     * @param {Array} meshes
     * @param {function} getData
     * @return {AttributeObject} attributeObject
     */
    addInstancedAttribute(attribute, meshes, getData) {
        this.bind();
        const gl = this.gl;
        const {
            name,
            glTypeInfo
        } = attribute;

        let instancedData = bufferUtil.getTypedArray(Float32Array, meshes.length * glTypeInfo.size);
        meshes.forEach((mesh, index) => {
            const attributeData = getData(mesh);
            if (attributeData !== undefined) {
                bufferUtil.fillArrayData(instancedData, getData(mesh), index * glTypeInfo.size);
            } else {
                log.warn('no attributeData:' + name + '-' + mesh.name);
            }
        });

        const attributeObject = this[name];
        let geometryData;
        if (attributeObject) {
            geometryData = attributeObject.geometryData;
            geometryData.data = instancedData;
        } else {
            geometryData = new GeometryData(instancedData, 1);
        }

        return this.addAttribute(geometryData, attribute, gl.DYNAMIC_DRAW, (attributeObject) => {
            attribute.divisor(1);
            attributeObject.useInstanced = true;
        });
    },

    /**
     * 获取资源
     * @param {Object[]} [resources=[]]
     * @return {Object[]}
     */
    getResources(resources = []) {
        if (this.attributes) {
            this.attributes.forEach((attributeObject) => {
                resources.push(attributeObject.buffer);
            });
        }

        if (this.indexBuffer) {
            resources.push(this.indexBuffer);
        }

        return resources;
    },
    /**
     * 没有被引用时销毁资源
     * @param  {WebGLRenderer} renderer
     * @return {VertexArrayObject} this
     */
    destroyIfNoRef(renderer) {
        const resourceManager = renderer.resourceManager;
        resourceManager.destroyIfNoRef(this);

        return this;
    },
    /**
     * 销毁资源
     * @return {VertexArrayObject} this
     */
    destroy() {
        if (this._isDestroyed) {
            return this;
        }

        this.instancedExtension = null;

        if (this.useVao) {
            this.vaoExtension.deleteVertexArray(this.vao);
            this.vao = null;
            this.vaoExtension = null;
        }
        this.gl = null;
        this.indexBuffer = null;
        this.attributes.forEach((attributeObject) => {
            const attribute = attributeObject.attribute || {};
            this[attribute.name] = null;
        });
        this.attributes = null;
        this.activeStates = null;
        cache.removeObject(this);

        this._isDestroyed = true;
        return this;
    }
});

export default VertexArrayObject;


/**
 * 顶点对象
 * @typedef {object} AttributeObject
 * @property {Object} attribute
 * @property {WebGLBuffer} buffer
 * @property {GeometryData} geometryData
 * @property {Boolean} useInstanced
 */