renderer_Renderer.ts

import {Camera} from "../camera/Camera";
import {Control} from "../control/Control";
import {cons} from "../cons";
import {createRendererEvents} from "./RendererEvents";
import type {IBaseInputState, RendererEventsHandler} from "./RendererEvents";
import {depth} from "../shaders/depth";
import {EntityCollection} from "../entity/EntityCollection";
import {Framebuffer, Multisample} from "../webgl/index";
import {FontAtlas} from "../utils/FontAtlas";
import {Handler} from "../webgl/Handler";
import type {WebGLBufferExt} from "../webgl/Handler";
import {input} from "../input/input";
import {isEmpty} from "../utils/shared";
import {LabelWorker} from "../entity/LabelWorker";
import {randomi} from "../math";
import {RenderNode} from "../scene/RenderNode";
import {screenFrame} from "../shaders/screenFrame";
import {toneMapping} from "../shaders/tone_mapping/toneMapping";
import {TextureAtlas} from "../utils/TextureAtlas";
import {Vec2} from "../math/Vec2";
import {Vec3} from "../math/Vec3";
import type {NumberArray3} from "../math/Vec3";
import {NumberArray4, Vec4} from "../math/Vec4";

interface IRendererParams {
    controls?: Control[];
    msaa?: number;
    autoActivate?: boolean;
    fontsSrc?: string;
    gamma?: number;
    exposure?: number;
    dpi?: number;
}

interface IPickingObject {
    _pickingColor?: Vec3;
    _pickingColorU?: Float32Array;
}

interface IFrameCallbackHandler {
    id: number;
    callback: Function;
    sender: any;
}

const MSAA_DEFAULT = 0;

let __pickingCallbackCounter__ = 0;

let __depthCallbackCounter__ = 0;

let _tempDepth_ = new Float32Array(2);

// function clientWaitAsync(gl: WebGL2RenderingContext, sync: WebGLSync, flags: number): Promise<void> {
//     return new Promise<void>((resolve, reject) => {
//         function check() {
//             const res = gl.clientWaitSync(sync, flags, 0);
//             if (res == gl.WAIT_FAILED) {
//                 reject();
//             } else if (res == gl.TIMEOUT_EXPIRED) {
//                 requestAnimationFrame(check);
//             } else {
//                 resolve();
//             }
//         }
//
//         check();
//     });
// }

/**
 * Represents high level WebGL context interface that starts WebGL handler working in real time.
 * @class
 * @param {Handler} handler - WebGL handler context.
 * @param {Object} [params] - Renderer parameters:
 * @fires RendererEventsHandler<RendererEventsType>#draw
 * @fires RendererEventsHandler<RendererEventsType>#resize
 * @fires RendererEventsHandler<RendererEventsType>#mousemove
 * @fires RendererEventsHandler<RendererEventsType>#mousestop
 * @fires RendererEventsHandler<RendererEventsType>#lclick
 * @fires RendererEventsHandler<RendererEventsType>#rclick
 * @fires RendererEventsHandler<RendererEventsType>#mclick
 * @fires RendererEventsHandler<RendererEventsType>#ldblclick
 * @fires RendererEventsHandler<RendererEventsType>#rdblclick
 * @fires RendererEventsHandler<RendererEventsType>#mdblclick
 * @fires RendererEventsHandler<RendererEventsType>#lup
 * @fires RendererEventsHandler<RendererEventsType>#rup
 * @fires RendererEventsHandler<RendererEventsType>#mup
 * @fires RendererEventsHandler<RendererEventsType>#ldown
 * @fires RendererEventsHandler<RendererEventsType>#rdown
 * @fires RendererEventsHandler<RendererEventsType>#mdown
 * @fires RendererEventsHandler<RendererEventsType>#lhold
 * @fires RendererEventsHandler<RendererEventsType>#rhold
 * @fires RendererEventsHandler<RendererEventsType>#mhold
 * @fires RendererEventsHandler<RendererEventsType>#mousewheel
 * @fires RendererEventsHandler<RendererEventsType>#touchstart
 * @fires RendererEventsHandler<RendererEventsType>#touchend
 * @fires RendererEventsHandler<RendererEventsType>#touchcancel
 * @fires RendererEventsHandler<RendererEventsType>#touchmove
 * @fires RendererEventsHandler<RendererEventsType>#doubletouch
 * @fires RendererEventsHandler<RendererEventsType>#touchleave
 * @fires RendererEventsHandler<RendererEventsType>#touchenter
 */

export interface HTMLDivElementExt extends HTMLDivElement {
    attributions?: HTMLElement;
}

class Renderer {

    /**
     * Div element with WebGL canvas. Assigned in Globe class.
     * @public
     * @type {HTMLElement | null}
     */
    public div: HTMLDivElementExt | null;

    /**
     * WebGL handler context.
     * @public
     * @type {Handler}
     */
    public handler: Handler;

    public exposure: number;
    public gamma: number;
    public whitepoint: number;
    public brightThreshold: number;

    /**
     * Render nodes drawing queue.
     * @public
     * @type {Array.<RenderNode>}
     */
    public _renderNodesArr: RenderNode[];

    /**
     * Render nodes store for the comfortable access by the node name.
     * @public
     * @type {Object.<RenderNode>}
     */
    public renderNodes: Record<string, RenderNode>;

    /**
     * Current active camera.
     * @public
     * @type {Camera}
     */
    public activeCamera: Camera;

    /**
     * Renderer events. Represents interface for setting events like mousemove, draw, keypress etc.
     * @public
     * @type {RendererEvents}
     */
    public events: RendererEventsHandler;

    /**
     * Controls array.
     * @public
     * @type {Object}
     */
    public controls: Record<string, Control>;

    /**
     * Provides exchange between controls.
     * @public
     * @type {any}
     */
    public controlsBag: any;

    /**
     * Hash table for drawing objects.
     * @public
     * @type {Map<string, any>}
     */
    public colorObjects: Map<string, any>;

    /**
     * Color picking objects rendering queue.
     * @type {Function[]}
     */
    protected _pickingCallbacks: IFrameCallbackHandler[];

    /**
     * Picking objects(labels and billboards) framebuffer.
     * @public
     * @type {Framebuffer}
     */
    public pickingFramebuffer: Framebuffer | null;

    /**
     * Depth objects rendering queue.
     * @type {Function[]}
     */
    protected _depthCallbacks: IFrameCallbackHandler[];

    public depthFramebuffer: Framebuffer | null;

    protected _msaa: number;

    protected _internalFormat: string;
    protected _format: string;
    protected _type: string;

    public sceneFramebuffer: Framebuffer | Multisample | null;

    protected blitFramebuffer: Framebuffer | null;

    protected toneMappingFramebuffer: Framebuffer | null;

    protected _initialized: boolean;

    /**
     * Texture atlas for the billboards images. One atlas per node.
     * @public
     * @type {TextureAtlas}
     */
    public billboardsTextureAtlas: TextureAtlas;

    /**
     * Texture atlas for the billboards images. One atlas per node.
     * @public
     * @type {TextureAtlas}
     */
    public geoObjectsTextureAtlas: TextureAtlas;

    /**
     * Texture font atlas for the font families and styles. One atlas per node.
     * @public
     * @type {FontAtlas}
     */
    public fontAtlas: FontAtlas;

    protected _entityCollections: EntityCollection[][];

    protected _currentOutput: string;

    protected _fnScreenFrame: Function | null;

    public labelWorker: LabelWorker;

    public screenDepthFramebuffer: Framebuffer | null;

    public screenFramePositionBuffer: WebGLBufferExt | null;

    public screenTexture: Record<string, WebGLTexture>;

    public outputTexture: WebGLTexture | null;

    protected _readPickingBuffer: () => void;

    constructor(handler: Handler | string | HTMLCanvasElement, params: IRendererParams = {}) {

        this.div = null;

        if (handler instanceof Handler) {
            this.handler = handler;
        } else {
            this.handler = new Handler(handler, {
                pixelRatio: params.dpi || (window.devicePixelRatio + 0.15),
                autoActivate: true
            });
        }

        this.exposure = params.exposure || 3.01;

        this.gamma = params.gamma || 0.47;

        this.whitepoint = 1.0;

        this.brightThreshold = 0.9;

        this._renderNodesArr = [];

        this.renderNodes = {};

        this.activeCamera = new Camera({
            width: this.handler.canvas?.width,
            height: this.handler.canvas?.height,
            eye: new Vec3(0, 0, 0),
            look: new Vec3(0, 0, -1),
            up: new Vec3(0, 1, 0)
        });

        this.events = createRendererEvents(this);

        this.controls = {};

        if (params.controls) {
            for (let i in params.controls) {
                this.controls[params.controls[i].name] = params.controls[i];
            }
        }

        this.controlsBag = {};

        this.colorObjects = new Map<string, any>();

        this._pickingCallbacks = [];

        this.pickingFramebuffer = null;

        this._depthCallbacks = [];

        this.depthFramebuffer = null;

        let urlParams = new URLSearchParams(location.search);
        let msaaParam = urlParams.get('og_msaa');
        if (msaaParam) {
            this._msaa = Number(urlParams.get('og_msaa'));
        } else {
            this._msaa = params.msaa != undefined ? params.msaa : MSAA_DEFAULT;
        }

        this._internalFormat = "RGBA16F";
        this._format = "RGBA";
        this._type = "FLOAT";

        this.sceneFramebuffer = null;

        this.blitFramebuffer = null;

        this.toneMappingFramebuffer = null;

        this._initialized = false;

        /**
         * Texture atlas for the billboards images. One atlas per node.
         * @public
         * @type {TextureAtlas}
         */
        this.billboardsTextureAtlas = new TextureAtlas();

        /**
         * Texture atlas for the billboards images. One atlas per node.
         * @public
         * @type {TextureAtlas}
         */
        this.geoObjectsTextureAtlas = new TextureAtlas();

        /**
         * Texture font atlas for the font families and styles. One atlas per node.
         * @public
         * @type {FontAtlas}
         */
        this.fontAtlas = new FontAtlas(params.fontsSrc);

        this._entityCollections = [[]];

        this._currentOutput = "screen";

        this._fnScreenFrame = null;

        this.labelWorker = new LabelWorker(4);

        this.screenDepthFramebuffer = null;

        this.screenFramePositionBuffer = null;

        this.screenTexture = {};

        this.outputTexture = null;

        this._readPickingBuffer = this._readPickingBuffer_webgl2;

        if (params.autoActivate || isEmpty(params.autoActivate)) {
            this.start();
        }
    }

    public enableBlendOneSrcAlpha() {
        let gl = this.handler.gl!;
        gl.enable(gl.BLEND);
        gl.blendEquation(gl.FUNC_ADD);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    }

    public enableBlendDefault() {
        let gl = this.handler.gl!;
        gl.enable(gl.BLEND);
        gl.blendEquation(gl.FUNC_ADD);
        gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
    }

    public setRelativeCenter(c?: Vec3) {
        this.events.dispatch(this.events.changerelativecenter, c || this.activeCamera.eye);
    }

    /**
     * Sets renderer events activity.
     * @param {Boolean} activity - Events activity.
     */
    public setEventsActivity(activity: boolean) {
        this.events.active = activity;
    }

    public addDepthCallback(sender: any, callback: Function) {
        let id = __depthCallbackCounter__++;
        this._depthCallbacks.push({
            id: id, callback: callback, sender: sender
        });
        return id;
    }

    public removeDepthCallback(id: number) {
        for (let i = 0; i < this._depthCallbacks.length; i++) {
            if (id === this._depthCallbacks[i].id) {
                this._depthCallbacks.splice(i, 1);
                break;
            }
        }
    }

    /**
     * Adds picking rendering callback function.
     * @param {object} sender - Callback context.
     * @param {Function} callback - Rendering callback.
     * @returns {Number} Handler id
     */
    public addPickingCallback(sender: any, callback: Function) {
        let id = __pickingCallbackCounter__++;
        this._pickingCallbacks.push({
            id: id, callback: callback, sender: sender
        });
        return id;
    }

    /**
     * Removes picking rendering callback function.
     * @param {Number} id - Handler id to remove.
     */
    public removePickingCallback(id: number) {
        for (let i = 0; i < this._pickingCallbacks.length; i++) {
            if (id === this._pickingCallbacks[i].id) {
                this._pickingCallbacks.splice(i, 1);
                break;
            }
        }
    }

    public getPickingObject<T>(r: number, g: number, b: number): T {
        return this.colorObjects.get(`${r}_${g}_${b}`);
    }

    public getPickingObjectArr<T>(arr: NumberArray3 | Uint8Array): T {
        return this.colorObjects.get(`${arr[0]}_${arr[1]}_${arr[2]}`);
    }

    public getPickingObject3v<T>(vec: Vec3 | Vec4): T {
        return this.colorObjects.get(`${vec.x}_${vec.y}_${vec.z}`);
    }

    /**
     * Assign picking color to the object.
     * @public
     * @param {Object} obj - Object that presume to be picked.
     */
    public assignPickingColor<T>(obj: T & IPickingObject) {
        if (!obj._pickingColor || obj._pickingColor.isZero()) {
            let r = 0, g = 0, b = 0;
            let str = "0_0_0";
            while (!(r || g || b) || this.colorObjects.has(str)) {
                r = randomi(1, 255);
                g = randomi(1, 255);
                b = randomi(1, 255);
                str = `${r}_${g}_${b}`;
            }

            if (!obj._pickingColor) {
                obj._pickingColor = new Vec3(r, g, b);
            } else {
                obj._pickingColor.set(r, g, b);
            }

            obj._pickingColorU = new Float32Array([r / 255, g / 255, b / 255]);

            this.colorObjects.set(str, obj);
        }
    }

    /**
     * Removes picking color from object.
     * @public
     * @param {Object} obj - Object to remove picking color.
     */
    public clearPickingColor<T>(obj: T & IPickingObject) {
        if (obj._pickingColor && !obj._pickingColor.isZero()) {
            let c = obj._pickingColor;
            if (!c.isZero()) {
                this.colorObjects.delete(`${c.x}_${c.y}_${c.z}`);
                c.x = c.y = c.z = 0;
            }
        }
    }

    /**
     * Get the client width.
     * @public
     * @returns {number} -
     */
    public getWidth(): number {
        return this.handler.canvas!.clientWidth;
    }

    /**
     * Get the client height.
     * @public
     * @returns {number} -
     */
    public getHeight(): number {
        return this.handler.canvas!.clientHeight;
    }

    /**
     * Get center of the canvas
     * @public
     * @returns {Vec2} -
     */
    public getCenter(): Vec2 {
        let cnv = this.handler.canvas!;
        return new Vec2(Math.round(cnv.width * 0.5), Math.round(cnv.height * 0.5));
    }

    /**
     * Get center of the screen viewport
     * @public
     * @returns {Vec2} -
     */
    public getClientCenter(): Vec2 {
        let cnv = this.handler.canvas!;
        return new Vec2(Math.round(cnv.clientWidth * 0.5), Math.round(cnv.clientHeight * 0.5));
    }

    /**
     * Add the given control to the renderer.
     * @param {Control} control - Control.
     */
    public addControl(control: Control) {
        control.addTo(this);
    }

    /**
     * Add the given controls array to the planet node.
     * @param {Array.<Control>} cArr - Control array.
     */
    public addControls(cArr: Control[]) {
        for (let i = 0; i < cArr.length; i++) {
            cArr[i].addTo(this);
        }
    }

    /**
     * Remove control from the renderer.
     * @param {Control} control  - Control.
     */
    public removeControl(control: Control) {
        control.remove();
    }

    public isInitialized(): boolean {
        return this._initialized;
    }

    /**
     * Renderer initialization.
     * @public
     */
    public initialize() {

        if (this._initialized) {
            return;
        } else {
            this._initialized = true;
        }

        this.handler.initialize();

        this.billboardsTextureAtlas.assignHandler(this.handler);
        this.geoObjectsTextureAtlas.assignHandler(this.handler);

        this.fontAtlas.assignHandler(this.handler);

        this.handler.setFrameCallback(() => {
            this.draw();
        });

        this.events.initialize();

        // Bind console key
        this.events.on("charkeypress", input.KEY_APOSTROPHE, function () {
            cons.setVisibility(!cons.getVisibility());
        });

        this.handler.addProgram(screenFrame());

        this.pickingFramebuffer = new Framebuffer(this.handler, {
            width: 640,
            height: 480,
            targets: [{
                readAsync: true
            }]
        });
        this.pickingFramebuffer.init();

        this.depthFramebuffer = new Framebuffer(this.handler, {
            width: 640,
            height: 480,
            targets: [{
                internalFormat: "RGBA",
                type: "UNSIGNED_BYTE",
                attachment: "COLOR_ATTACHMENT",
                readAsync: true
            }, {
                internalFormat: "RGBA16F",
                type: "FLOAT",
                attachment: "COLOR_ATTACHMENT",
                readAsync: true
            }],
            useDepth: true
        });

        this.depthFramebuffer.init();

        this.screenDepthFramebuffer = new Framebuffer(this.handler, {
            useDepth: false
        });
        this.screenDepthFramebuffer.init();

        if (this.handler.gl!.type === "webgl") {
            this._readPickingBuffer = this._readPickingBuffer_webgl1;

            this.sceneFramebuffer = new Framebuffer(this.handler);
            this.sceneFramebuffer.init();

            this._fnScreenFrame = this._screenFrameNoMSAA;

            this.screenTexture = {
                screen: this.sceneFramebuffer!.textures[0],
                picking: this.pickingFramebuffer!.textures[0],
                depth: this.screenDepthFramebuffer!.textures[0]
            };
        } else {
            let _maxMSAA = this.getMaxMSAA(this._internalFormat);

            if (this._msaa > _maxMSAA) {
                this._msaa = _maxMSAA;
            }

            this.handler.addPrograms([toneMapping()]);

            this.handler.addPrograms([depth()]);

            this.sceneFramebuffer = new Multisample(this.handler, {
                size: 1,
                msaa: this._msaa,
                internalFormat: this._internalFormat,
                filter: "LINEAR"
            });

            this.sceneFramebuffer.init();

            this.blitFramebuffer = new Framebuffer(this.handler, {
                size: 1,
                useDepth: false,
                targets: [{
                    internalFormat: this._internalFormat,
                    format: this._format,
                    type: this._type,
                    filter: "NEAREST"
                }]
            });

            this.blitFramebuffer.init();

            this.toneMappingFramebuffer = new Framebuffer(this.handler, {
                useDepth: false
            });

            this.toneMappingFramebuffer.init();

            this._fnScreenFrame = this._screenFrameMSAA;

            this.screenTexture = {
                screen: this.toneMappingFramebuffer!.textures[0],
                picking: this.pickingFramebuffer!.textures[0],
                depth: this.screenDepthFramebuffer!.textures[0],
                frustum: this.depthFramebuffer!.textures[0]
            };
        }

        this.handler.ONCANVASRESIZE = () => {
            this._resizeStart();
            this.events.dispatch(this.events.resize, this.handler.canvas);
            this._resizeEnd();
            //clearTimeout(__resizeTimeout);
            // __resizeTimeout = setTimeout(() => {
            //     this._resizeEnd();
            //     this.events.dispatch(this.events.resizeend, this.handler.canvas);
            // }, 320);
            this.events.dispatch(this.events.resizeend, this.handler.canvas);
        };

        this.screenFramePositionBuffer = this.handler.createArrayBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), 2, 4);

        this.outputTexture = this.screenTexture.screen;

        this._initializeRenderNodes();

        this._initializeControls();
    }

    public _initializeControls() {
        let temp = this.controls;
        this.controls = {};
        for (let i in temp) {
            this.addControl(temp[i]);
        }
    }

    public resize() {
        this._resizeEnd();
    }

    public setCurrentScreen(screenName: string) {
        this._currentOutput = screenName;
        if (this.screenTexture[screenName]) {
            this.outputTexture = this.screenTexture[screenName];
        }
    }

    public _resizeStart() {
        let c = this.handler.canvas!;

        this.activeCamera!.setViewportSize(c.width, c.height);
        this.sceneFramebuffer!.setSize(c.width * 0.5, c.height * 0.5);
        this.blitFramebuffer && this.blitFramebuffer.setSize(c.width * 0.5, c.height * 0.5, true);
    }

    public _resizeEnd() {
        let c = this.handler.canvas!;

        this.activeCamera!.setViewportSize(c.width, c.height);
        this.sceneFramebuffer!.setSize(c.width, c.height);
        this.blitFramebuffer && this.blitFramebuffer.setSize(c.width, c.height, true);

        this.toneMappingFramebuffer && this.toneMappingFramebuffer.setSize(c.width, c.height, true);
        this.screenDepthFramebuffer && this.screenDepthFramebuffer.setSize(c.clientWidth, c.clientHeight, true);
        //this.depthFramebuffer && this.depthFramebuffer.setSize(c.clientWidth, c.clientHeight, true);

        if (this.handler.gl!.type === "webgl") {
            this.screenTexture.screen = (this.sceneFramebuffer as Framebuffer)!.textures[0];
            this.screenTexture.picking = this.pickingFramebuffer!.textures[0];
            this.screenTexture.depth = this.screenDepthFramebuffer!.textures[0];
            this.screenTexture.frustum = this.depthFramebuffer!.textures[0];
        } else {
            this.screenTexture.screen = this.toneMappingFramebuffer!.textures[0];
            this.screenTexture.picking = this.pickingFramebuffer!.textures[0];
            this.screenTexture.depth = this.screenDepthFramebuffer!.textures[0];
            this.screenTexture.frustum = this.depthFramebuffer!.textures[0];
        }

        this.setCurrentScreen(this._currentOutput);
    }

    public removeNode(renderNode: RenderNode) {
        // TODO: replace from RenderNode to this method
        renderNode.remove();
    }

    /**
     * Adds render node to the renderer.
     * @public
     * @param {RenderNode} renderNode - Render node.
     */
    public addNode(renderNode: RenderNode) {
        if (!this.renderNodes[renderNode.name]) {
            renderNode.assign(this);
            this._renderNodesArr.unshift(renderNode);
            this.renderNodes[renderNode.name] = renderNode;
        } else {
            cons.logWrn(`Node name ${renderNode.name} already exists.`);
        }
    }

    protected _initializeRenderNodes() {
        for (let i = 0; i < this._renderNodesArr.length; i++) {
            this._renderNodesArr[i].initialize();
        }
    }

    /**
     * Adds render node to the renderer before specific node.
     * @public
     * @param {RenderNode} renderNode - Render node.
     * @param {RenderNode} renderNodeBefore - Insert before the renderNodeBefore node.
     */
    public addNodeBefore(renderNode: RenderNode, renderNodeBefore: RenderNode) {
        if (!this.renderNodes[renderNode.name]) {
            renderNode.assign(this);
            this.renderNodes[renderNode.name] = renderNode;
            for (let i = 0; i < this._renderNodesArr.length; i++) {
                if (this._renderNodesArr[i].isEqual(renderNodeBefore)) {
                    this._renderNodesArr.splice(i, 0, renderNode);
                    break;
                }
            }
            this._renderNodesArr.unshift(renderNode);
        } else {
            cons.logWrn(`Node name ${renderNode.name} already exists.`);
        }
    }

    /**
     * Adds render nodes array to the renderer.
     * @public
     * @param {Array.<RenderNode>} nodesArr - Render nodes array.
     */
    public addNodes(nodesArr: RenderNode[]) {
        for (let i = 0; i < nodesArr.length; i++) {
            this.addNode(nodesArr[i]);
        }
    }

    public getMaxMSAA(internalFormat: string) {
        let gl = this.handler.gl!;
        let samples = gl.getInternalformatParameter(gl.RENDERBUFFER, (gl as any)[internalFormat], gl.SAMPLES);
        return samples[0];
    }

    public getMSAA(): number {
        return this._msaa;
    }

    /**
     * TODO: replace with cache friendly linked list by BillboardHandler, LabelHandler etc.
     */
    public enqueueEntityCollectionsToDraw(ecArr: EntityCollection[], depthOrder: number = 0) {
        if (!this._entityCollections[depthOrder]) {
            this._entityCollections[depthOrder] = [];
        }
        this._entityCollections[depthOrder].push(...ecArr);
    }

    /**
     * @protected
     */
    protected _drawEntityCollections(depthOrder: number) {
        let ec = this._entityCollections[depthOrder];

        if (ec.length) {
            let gl = this.handler.gl!;

            this.enableBlendDefault();

            // Point Clouds
            let i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].pointCloudHandler.draw();
            }

            // GeoObjects
            i = ec.length;
            while (i--) {
                let eci = ec[i];
                if (ec[i]._fadingOpacity) {
                    eci.events.dispatch(eci.events.draw, eci);
                    ec[i].geoObjectHandler.draw();
                }
            }

            // billboards pass
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, this.billboardsTextureAtlas.texture!);

            i = ec.length;
            while (i--) {
                let eci = ec[i];
                eci._fadingOpacity && eci.billboardHandler.draw();
            }

            // labels pass
            let fa = this.fontAtlas.atlasesArr;
            for (i = 0; i < fa.length; i++) {
                gl.activeTexture(gl.TEXTURE0 + i);
                gl.bindTexture(gl.TEXTURE_2D, fa[i].texture!);
            }

            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].labelHandler.draw();
            }

            // rays
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].rayHandler.draw();
            }

            // polyline pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].polylineHandler.draw();
            }

            // Strip pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].stripHandler.draw();
            }
        }
    }

    protected _drawPickingEntityCollections(depthOrder: number) {
        let ec = this._entityCollections[depthOrder];
        if (ec.length) {
            // billboard pass
            let i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].billboardHandler.drawPicking();
            }
            // geoObject pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].geoObjectHandler.drawPicking();
            }
            // label pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].labelHandler.drawPicking();
            }
            // ray pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].rayHandler.drawPicking();
            }
            // polyline pass
            i = ec.length;
            while (i--) {
                ec[i]._visibility && ec[i].polylineHandler.drawPicking();
            }
            //Strip pass
            i = ec.length;
            while (i--) {
                ec[i]._visibility && ec[i].stripHandler.drawPicking();
            }
            // //pointClouds pass
            // i = ec.length;
            // while (i--) {
            //    ec[i]._visibility && ec[i].pointCloudHandler.drawPicking();
            // }
        }
    }

    protected _drawDepthEntityCollections(depthOrder: number) {
        let ec = this._entityCollections[depthOrder];
        if (ec.length) {
            // geoObject pass
            let i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].geoObjectHandler.drawDepth();
            }

            // i = ec.length;
            // while (i--) {
            //     ec[i]._fadingOpacity && ec[i].rayHandler.drawDepth();
            // }
            //
            // // polyline pass
            // i = ec.length;
            // while (i--) {
            //     ec[i]._visibility && ec[i].polylineHandler.drawDepth();
            // }
            //
            // //Strip pass
            // i = ec.length;
            // while (i--) {
            //     ec[i]._visibility && ec[i].stripHandler.drawDepth();
            // }
        }
    }

    protected _clearEntityCollectionQueue(depthOrder: number) {
        this._entityCollections[depthOrder].length = 0;
        this._entityCollections[depthOrder] = [];
    }

    /**
     * Draw nodes.
     * @public
     */
    public draw() {
        this.activeCamera!.checkMoveEnd();

        let e = this.events;
        e.handleEvents();

        let sceneFramebuffer = this.sceneFramebuffer!;
        sceneFramebuffer.activate();

        let h = this.handler,
            gl = h.gl!;

        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        this.enableBlendDefault();

        e.dispatch(e.draw, this);

        let frustums = this.activeCamera!.frustums;

        let pointerEvent = e.pointerEvent();
        let mouseFree = !e.mouseState.leftButtonDown && !e.mouseState.rightButtonDown;

        // Rendering scene nodes and entityCollections
        let rn = this._renderNodesArr;
        let k = frustums.length;

        //
        // RenderNodes PASS
        //
        while (k--) {
            this.activeCamera!.setCurrentFrustum(k);
            gl.clear(gl.DEPTH_BUFFER_BIT);

            let i = rn.length;
            while (i--) {
                rn[i].preDrawNode();
            }

            i = rn.length;
            while (i--) {
                this.enableBlendDefault();
                rn[i].drawNode();
            }

            this._drawEntityCollections(0);

            e.dispatch(e.drawtransparent, this);

            if (pointerEvent && mouseFree) {
                this._drawPickingBuffer(0);
            }

            this._drawDepthBuffer(0);

            this._clearEntityCollectionQueue(0);
        }

        //
        // EntityCollections PASS
        //
        for (let i = 1; i < this._entityCollections.length; i++) {
            gl.clear(gl.DEPTH_BUFFER_BIT);
            let k = frustums.length;
            while (k--) {
                this.activeCamera!.setCurrentFrustum(k);

                this._drawEntityCollections(i);

                if (pointerEvent && mouseFree) {
                    this._drawPickingBuffer(i);
                }

                this._drawDepthBuffer(i);
            }

            this._clearEntityCollectionQueue(i);
        }

        sceneFramebuffer.deactivate();

        this.blitFramebuffer && (sceneFramebuffer as Multisample).blitTo(this.blitFramebuffer, 0);

        if (pointerEvent && mouseFree) {
            this._readPickingBuffer();
        }

        if (mouseFree) {
            this._readDepthBuffer();
        }

        // Tone mapping followed by rendering on the screen
        this._fnScreenFrame!();

        e.dispatch(e.postdraw, this);

        e.mouseState.wheelDelta = 0;
        e.mouseState.justStopped = false;
        e.mouseState.moving = false;
        e.touchState.moving = false;
    }

    public getImageDataURL(type: string = "image/png", quality: number = 1.0): string {
        this.draw();
        return this.handler.canvas ? this.handler.canvas.toDataURL(type, quality) : "";
    }

    protected _screenFrameMSAA() {
        let h = this.handler;

        let sh = h.programs.toneMapping,
            p = sh._program,
            gl = h.gl!;

        gl.disable(gl.DEPTH_TEST);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.screenFramePositionBuffer!);
        gl.vertexAttribPointer(p.attributes.corners, 2, gl.FLOAT, false, 0, 0);

        this.toneMappingFramebuffer!.activate();

        sh.activate();

        // screen texture
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.blitFramebuffer!.textures[0]);
        gl.uniform1i(p.uniforms.hdrBuffer, 0);

        gl.uniform1f(p.uniforms.gamma, this.gamma);
        gl.uniform1f(p.uniforms.exposure, this.exposure);
        gl.uniform1f(p.uniforms.whitepoint, this.whitepoint);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        this.toneMappingFramebuffer!.deactivate();

        // SCREEN PASS
        sh = h.programs.screenFrame;
        p = sh._program;
        sh.activate();
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.outputTexture);
        gl.uniform1i(p.uniforms.texture, 0);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        gl.enable(gl.DEPTH_TEST);
    }

    protected _screenFrameNoMSAA() {

        let h = this.handler;
        let sh = h.programs.screenFrame,
            p = sh._program,
            gl = h.gl!;

        gl.disable(gl.DEPTH_TEST);
        sh.activate();
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.outputTexture);
        gl.uniform1i(p.uniforms.texture, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, this.screenFramePositionBuffer!);
        gl.vertexAttribPointer(p.attributes.corners, 2, gl.FLOAT, false, 0, 0);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        gl.enable(gl.DEPTH_TEST);
    }

    /**
     * Draw picking objects framebuffer.
     * @private
     */
    protected _drawPickingBuffer(depthOrder: number) {
        this.pickingFramebuffer!.activate();

        let h = this.handler;
        let gl = h.gl!;

        if (this.activeCamera!.isFirstPass && depthOrder === 0) {
            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        } else {
            gl.clear(gl.DEPTH_BUFFER_BIT);
        }

        //
        // draw picking scenes, usually we don't need blending,
        // but sometimes set it manually in the callbacks
        //
        gl.disable(gl.BLEND);

        if (depthOrder === 0) {
            let dp = this._pickingCallbacks;
            for (let i = 0, len = dp.length; i < len; i++) {
                /**
                 * This callback renders picking frame.
                 */
                dp[i].callback.call(dp[i].sender);
            }
        }

        this._drawPickingEntityCollections(depthOrder);

        gl.enable(gl.BLEND);

        this.pickingFramebuffer!.deactivate();
    }

    protected _drawDepthBuffer(depthOrder: number) {
        this.depthFramebuffer!.activate();

        let h = this.handler;
        let gl = h.gl!;

        gl.disable(gl.BLEND);

        if (this.activeCamera!.isFirstPass && depthOrder === 0) {
            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        } else {
            gl.clear(gl.DEPTH_BUFFER_BIT);
        }

        if (depthOrder === 0) {
            let dp = this._depthCallbacks;
            let i = dp.length;
            while (i--) {
                /**
                 * This callback renders depth frame.
                 */
                dp[i].callback.call(dp[i].sender);
            }
        }

        this._drawDepthEntityCollections(depthOrder);

        this.depthFramebuffer!.deactivate();

        //
        // DEBUG SCREEN OUTPUTS
        //
        // if (this._currentOutput === "depth" || this._currentOutput === "frustum") {
        //     //
        //     // PASS to depth visualization
        //     this.screenDepthFramebuffer!.activate();
        //     let sh = h.programs.depth,
        //         p = sh._program;
        //
        //     gl.bindBuffer(gl.ARRAY_BUFFER, this.screenFramePositionBuffer!);
        //     gl.vertexAttribPointer(p.attributes.corners, 2, gl.FLOAT, false, 0, 0);
        //
        //     sh.activate();
        //
        //     gl.activeTexture(gl.TEXTURE0);
        //     gl.bindTexture(gl.TEXTURE_2D, this.depthFramebuffer!.textures[1]);
        //     gl.uniform1i(p.uniforms.depthTexture, 0);
        //
        //     gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        //
        //     this.screenDepthFramebuffer!.deactivate();
        //     gl.enable(gl.BLEND);
        // }
    }

    protected _readDepthBuffer() {
        this.depthFramebuffer!.readPixelBuffersAsync();
    }

    protected _readPickingBuffer_webgl1() {
        this.pickingFramebuffer!.readPixelBuffersAsync();
    }

    protected _readPickingBuffer_webgl2() {
        this.pickingFramebuffer!.readPixelBuffersAsync();
    }

    public readPickingColor(x: number, y: number, outColor: NumberArray3 | Uint8Array) {
        let w = this.pickingFramebuffer!.width;
        let h = this.pickingFramebuffer!.height;

        x = Math.round(x * w);
        y = Math.round(y * h);

        let ind = (y * w + x) * 4;

        let _tempPickingPix_ = this.pickingFramebuffer?.pixelBuffers[0].data;

        if (_tempPickingPix_) {
            outColor[0] = _tempPickingPix_[ind];
            outColor[1] = _tempPickingPix_[ind + 1];
            outColor[2] = _tempPickingPix_[ind + 2];
        }
    }

    public readDepth(x: number, y: number, outDepth: NumberArray3 | Float32Array) {

        // let depthFramebuffer = this.depthFramebuffer!;
        //
        // let w = depthFramebuffer.width;
        // let h = depthFramebuffer.height;
        //
        // let sx = Math.round(x * w);
        // let sy = Math.round(y * h);
        //
        // let ind = (sy * w + sx) * 4;
        //
        // let _tempDepthPix_ = depthFramebuffer.pixelBuffers[1].data;
        // let _tempFrustumPix_ = depthFramebuffer.pixelBuffers[0].data!;
        //
        // if (_tempDepthPix_) {
        //     outDepth[0] = _tempDepthPix_[ind];
        //     outDepth[1] = Math.round(_tempFrustumPix_[ind] / 10.0) - 1.0; // See Camera.frustumColorIndex
        // }

        //////

        let ddd = new Float32Array(4);
        let fff = new Uint8Array(4);

        this.depthFramebuffer!.readData(x, y, fff, 0);
        this.depthFramebuffer!.readData(x, y, ddd, 1);

        outDepth[0] = ddd[0];
        outDepth[1] = Math.round(fff[0] / 10.0) - 1.0; // See Camera.frustumColorIndex
    }

    /**
     * Returns the distance from the active (screen) camera to the 3d-surface using the defined screen coordinates
     * @public
     * @param {Vec2 | IBaseInputState} px - Screen coordinates.
     * @returns {number | undefined} -
     */
    public getDistanceFromPixel(px: Vec2 | IBaseInputState): number | undefined {

       let camera = this.activeCamera!;

        let cnv = this.handler!.canvas!;

        let nx = px.x / cnv.width;
        let ny = (cnv.height - px.y) / cnv.height;

        _tempDepth_[0] = _tempDepth_[1] = 0.0;

        let dist = 0;

        this.readDepth(nx, ny, _tempDepth_);

        if (_tempDepth_[1] === -1) {
            return;
        }

        let depth = _tempDepth_[0],
            frustum = camera.frustums[_tempDepth_[1]];

        if (!frustum) return;

        let screenPos = new Vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
        let viewPosition = frustum.inverseProjectionMatrix.mulVec4(screenPos);
        let dir = (px as IBaseInputState).direction || camera.unproject(px.x, px.y);
        dist = -(viewPosition.z / viewPosition.w) / dir.dot(camera.getForward());

        return dist;
    }

    /**
     * Returns 3d coordinates from screen coordinates
     * @public
     * @param {Vec2 | IBaseInputState} px - Screen coordinates.
     * @returns {Vec3 | undefined} -
     */
    public getCartesianFromPixel(px: Vec2 | IBaseInputState): Vec3 | undefined {
        let distance = this.getDistanceFromPixel(px);
        if (distance) {
            let direction = (px as IBaseInputState).direction || this.activeCamera.unproject(px.x, px.y);
            return direction.scaleTo(distance).addA(this.activeCamera.eye);
        }
    }

    /**
     * Function starts renderer
     * @public
     */
    public start() {
        if (!this._initialized) {
            this.initialize();
        }
        this.handler.start();
    }

    public destroy() {
        for (let i in this.controls) {
            this.controls[i].remove();
        }

        for (let i = 0; i < this._renderNodesArr.length; i++) {
            this._renderNodesArr[i].remove();
        }

        this.div = null;

        this._renderNodesArr = [];

        this.renderNodes = {};

        //@ts-ignore
        this.activeCamera = null;

        this.controls = {};

        this.controlsBag = {};

        this.colorObjects.clear();

        // @ts-ignore
        this.colorObjects = null;

        this._pickingCallbacks = [];

        this.pickingFramebuffer = null;

        //@ts-ignore
        this._tempPickingPix_ = null;

        this._depthCallbacks = [];

        this.depthFramebuffer = null;

        this.sceneFramebuffer = null;

        this.blitFramebuffer = null;

        this.toneMappingFramebuffer = null;

        // todo
        //this.billboardsTextureAtlas.clear();
        //this.geoObjectsTextureAtlas.clear()
        //this.fontAtlas.clear();

        this._entityCollections = [[]];

        this.handler.ONCANVASRESIZE = null;
        this.handler.destroy();

        // @ts-ignore
        this.handler = null;

        this._initialized = false;
    }

}

export {Renderer};