utils/VectorTileCreator.tstypescript
import { doubleToTwoFloats2 } from "../math/doubleToTwoFloats";
import { Framebuffer } from "../webgl/Framebuffer";
import { Handler } from "../webgl/Handler";
import { Material } from "../layer/Material";
import { ShaderProgram } from "../webgl/ShaderProgram";
import { Planet } from "../scene/Planet";
import { NOTRENDERING } from "../quadTree/quadTree";
import { Vector } from "../layer/Vector";
import { CUBE_FACES, CubeProj, isCubeProj } from "../proj/cube";
import type { Segment } from "../segment/Segment";

//
//@todo: single pass vector rendering
//

let tempArr = new Float32Array(2);

const MAX_FRAME_TIME = 50.0;

// The draw calls take no time here, the GPU does them later. Every tile draws all the vertices of its layer.
const MAX_FRAME_VERTICES = 5000000;

// The tile is a rectangle of longitudes and latitudes taken in the Web Mercator coordinates, as the vertices are: the
// Mercator tiles, the polar caps of the Earth and the tiles of the EPSG4326, Equi and Wgs84 strategies
const EXTENT_PROJ = `uniform vec4 extentParamsHigh;
                uniform vec4 extentParamsLow;

                vec2 proj(vec2 coordHigh, vec2 coordLow, out float w) {
                    w = 1.0;
                    vec2 highDiff = coordHigh - extentParamsHigh.xy;
                    vec2 lowDiff = coordLow - extentParamsLow.xy;
                    return vec2(-1.0 + (highDiff * step(1.0, length(highDiff)) + lowDiff) * extentParamsHigh.zw) * vec2(1.0, -1.0);
                }`;

// The tile is a rectangle on a plane and the vertices are directions from the planet center. The offset q from the
// direction of the tile center is small, so floats keep the precision. An equiangular tile is a rectangle of angles.
// w is the distance to the plane, a triangle with a vertex behind the plane is clipped by it as a camera does, the
// vertex behind goes by its plane coordinates, so the clipped part goes away from the tile.
const PLANE_PROJ = `uniform vec3 centerHigh;
                uniform vec3 centerLow;
                uniform vec3 axisU;
                uniform vec3 axisV;
                uniform vec3 axisC;
                uniform vec2 center;
                uniform vec2 scale;
                uniform float equiangular;

                vec2 proj(vec3 coordHigh, vec3 coordLow, out float w) {
                    if (dot(coordHigh, coordHigh) == 0.0) {
                        w = -1.0;
                        return vec2(0.0);
                    }
                    vec3 q = (coordHigh - centerHigh) + (coordLow - centerLow);
                    float qc = dot(q, axisC);
                    float pc = dot(centerHigh, axisC) + dot(centerLow, axisC) + qc;
                    if (abs(pc) < 1e-7) {
                        pc = -1e-7;
                    }
                    w = pc;
                    vec2 d = vec2(dot(q, axisU) - center.x * qc, dot(q, axisV) - center.y * qc) / pc;
                    if (equiangular > 0.5 && pc > 0.0) {
                        d = vec2(atan(d.x, 1.0 + center.x * (center.x + d.x)), atan(d.y, 1.0 + center.y * (center.y + d.y)));
                    }
                    return d * scale * vec2(1.0, -1.0);
                }`;

const EXTENT_UNIFORMS = {
    extentParamsHigh: "vec4",
    extentParamsLow: "vec4"
};

const PLANE_UNIFORMS = {
    centerHigh: "vec3",
    centerLow: "vec3",
    axisU: "vec3",
    axisV: "vec3",
    axisC: "vec3",
    center: "vec2",
    scale: "vec2",
    equiangular: "float"
};

const lineVertexShader = (coord: string, proj: string) => `attribute ${coord} prevHigh;
                attribute ${coord} currentHigh;
                attribute ${coord} nextHigh;

                attribute ${coord} prevLow;
                attribute ${coord} currentLow;
                attribute ${coord} nextLow;

                attribute float order;
                attribute float thickness;
                attribute vec4 color;
                uniform float thicknessOutline;
                uniform vec2 viewport;
                varying vec4 vColor;
${proj}

                vec2 getIntersection(vec2 start1, vec2 end1, vec2 start2, vec2 end2, float eps) {
                    vec2 dir = end2 - start2;
                    vec2 perp = vec2(-dir.y, dir.x);
                    float d2 = dot(perp, start2);
                    float seg = dot(perp, start1) - d2;
                    float prl = seg - dot(perp, end1) + d2;
                    if (prl > -eps && prl < eps) {
                        return start1;
                    }
                    float u = seg / prl;
                    return start1 + u * (end1 - start1);
                }

                vec2 safeNormalize(vec2 v, vec2 fallback, float eps) {
                    float len2 = dot(v, v);
                    if (len2 > eps * eps) {
                        return v * inversesqrt(len2);
                    }
                    return fallback;
                }

                void main(){
                    vColor = color;
                    float eps = 2.0 / max(viewport.x, viewport.y);

                    float w, wNext, wPrev;
                    vec2 vNext = proj(nextHigh, nextLow, wNext),
                         vCurrent = proj(currentHigh, currentLow, w),
                         vPrev = proj(prevHigh, prevLow, wPrev);

                    vec2 _next = vNext;
                    vec2 _prev = vPrev;
                    vec2 _current = vCurrent;

                    if(distance(_prev, _current) < eps){
                        if(distance(_next, _current) < eps){
                            _next = _current + vec2(1.0, 0.0);
                            _prev = _current - vec2(1.0, 0.0);
                        }else{
                            _prev = _current + safeNormalize(_current - _next, vec2(1.0, 0.0), eps);
                        }
                    }

                    if(distance(_next, _current) < eps){
                        _next = _current + safeNormalize(_current - _prev, vec2(-1.0, 0.0), eps);
                    }

                    vec2 sNext = _next;
                    vec2 sCurrent = _current;
                    vec2 sPrev = _prev;

                    vec2 dirNext = safeNormalize(sNext - sCurrent, vec2(1.0, 0.0), eps);
                    vec2 dirPrev = safeNormalize(sPrev - sCurrent, -dirNext, eps);
                    float dotNP = clamp(dot(dirNext, dirPrev), -1.0, 1.0);

                    vec2 normalNext = safeNormalize(vec2(-dirNext.y, dirNext.x), vec2(0.0, 1.0), eps);
                    vec2 normalPrev = safeNormalize(vec2(dirPrev.y, -dirPrev.x), vec2(0.0, 1.0), eps);
                    vec2 sideNormal = abs(order) == 1.0 ? normalPrev : normalNext;
                    float side = sign(order);
                    vec2 d = (thickness + thicknessOutline) * 0.5 * side / viewport;

                    vec2 m;
                    float lenNext = distance(sCurrent, sNext);
                    float lenPrev = distance(sCurrent, sPrev);
                    if(lenNext < eps || lenPrev < eps){
                        m = sCurrent + sideNormal * d;
                    }else if(dotNP >= 0.99991){
                        m = sCurrent - normalPrev * d;
                    }else{
                        m = getIntersection(sCurrent + normalPrev * d, sPrev + normalPrev * d,
                                            sCurrent + normalNext * d, sNext + normalNext * d, eps);

                        if( dotNP > 0.5 && dot(dirNext + dirPrev, m - sCurrent) < 0.0 ){
                            float occw = order * sign(dirNext.x * dirPrev.y - dirNext.y * dirPrev.x);
                            if(occw == -1.0){
                                m = sCurrent + normalPrev * d;
                            }else if(occw == 1.0){
                                m = sCurrent + normalNext * d;
                            }else if(occw == -2.0){
                                m = sCurrent + normalNext * d;
                            }else if(occw == 2.0){
                                m = sCurrent + normalPrev * d;
                            }
                        }else if(distance(sCurrent, m) > min(distance(sCurrent, sNext), distance(sCurrent, sPrev))){
                            m = sCurrent + sideNormal * d;
                        }
                    }
                    float sameSide = dot(m - sCurrent, sideNormal) * side;
                    float wrongSide = 1.0 - step(0.0, sameSide);
                    m = mix(m, sCurrent + sideNormal * d, wrongSide);
                    gl_Position = vec4(m * w, 0.0, w);
                }`;

const LINE_FRAGMENT_SHADER = `precision highp float;
                uniform float alpha;
                varying vec4 vColor;
                void main() {
                    gl_FragColor = vec4(vColor.rgb, alpha * vColor.a);
                }`;

const polygonVertexShader = (coord: string, proj: string) => `attribute ${coord} coordinatesHigh;
                attribute ${coord} coordinatesLow;
                attribute vec4 colors;
                varying vec4 color;
${proj}

                void main() {
                    color = colors;
                    float w;
                    vec2 p = proj(coordinatesHigh, coordinatesLow, w);
                    gl_Position = vec4(p * w, 0.0, w);
                }`;

const POLYGON_FRAGMENT_SHADER = `precision highp float;
                varying vec4 color;
                void main () {
                    gl_FragColor = color;
                }`;

function lineProgram(name: string, coord: string, proj: string, uniforms: Record<string, string>) {
    return new ShaderProgram(name, {
        uniforms: {
            viewport: "vec2",
            thicknessOutline: "float",
            alpha: "float",
            ...uniforms
        },
        attributes: {
            prevHigh: coord,
            currentHigh: coord,
            nextHigh: coord,

            prevLow: coord,
            currentLow: coord,
            nextLow: coord,

            order: "float",
            color: "vec4",
            thickness: "float"
        },
        vertexShader: lineVertexShader(coord, proj),
        fragmentShader: LINE_FRAGMENT_SHADER
    });
}

function polygonProgram(name: string, coord: string, proj: string, uniforms: Record<string, string>) {
    return new ShaderProgram(name, {
        uniforms,
        attributes: {
            coordinatesHigh: coord,
            coordinatesLow: coord,
            colors: "vec4"
        },
        vertexShader: polygonVertexShader(coord, proj),
        fragmentShader: POLYGON_FRAGMENT_SHADER
    });
}

// The plane of the tile
const _plane = {
    centerHigh: new Float32Array(3),
    centerLow: new Float32Array(3),
    axisU: new Float32Array(3),
    axisV: new Float32Array(3),
    axisC: new Float32Array(3),
    center: new Float32Array(2),
    scale: new Float32Array(2),
    equiangular: 0
};

export class VectorTileCreator {
    protected _width: number;
    protected _height: number;
    protected _planet: Planet;
    protected _framebuffer: Framebuffer | null;
    protected _queue: Material[];
    protected _handler: Handler | null;

    /**
     * Vertices the tiles of one frame draw, all the passes. A frame draws one tile at least.
     * @public
     * @type {number}
     */
    public maxFrameVertices: number;

    constructor(planet: Planet, width: number = 256, height: number = 256) {
        this._width = width;
        this._height = height;
        this._planet = planet;
        this._framebuffer = null;
        this._queue = [];
        this._handler = null;
        this.maxFrameVertices = MAX_FRAME_VERTICES;
    }

    public init() {
        this._handler = this._planet.renderer!.handler;

        const programs: [string, () => ShaderProgram][] = [
            [
                "vectorTileLineRasterization",
                () => lineProgram("vectorTileLineRasterization", "vec2", EXTENT_PROJ, EXTENT_UNIFORMS)
            ],
            [
                "vectorTilePolygonRasterization",
                () => polygonProgram("vectorTilePolygonRasterization", "vec2", EXTENT_PROJ, EXTENT_UNIFORMS)
            ],
            [
                "vectorTileLineRasterizationPlane",
                () => lineProgram("vectorTileLineRasterizationPlane", "vec3", PLANE_PROJ, PLANE_UNIFORMS)
            ],
            [
                "vectorTilePolygonRasterizationPlane",
                () => polygonProgram("vectorTilePolygonRasterizationPlane", "vec3", PLANE_PROJ, PLANE_UNIFORMS)
            ]
        ];

        for (const [name, create] of programs) {
            if (!this._handler.programs[name]) {
                this._handler.addProgram(create());
            }
        }

        this._framebuffer = new Framebuffer(this._handler, {
            width: this._width,
            height: this._height,
            useDepth: false
        });

        this._framebuffer.init();
    }

    /**
     * Returns true when there is nothing left to rasterize.
     * @public
     * @returns {boolean}
     */
    public get isIdle(): boolean {
        return this._queue.length === 0;
    }

    public frame() {
        if (this._planet.layerLock.isFree() && this._queue.length) {
            let h = this._handler!,
                gl = h.gl!;

            gl.disable(gl.CULL_FACE);
            gl.disable(gl.DEPTH_TEST);

            let hLine = h.programs.vectorTileLineRasterization,
                hPoly = h.programs.vectorTilePolygonRasterization;

            let _w = this._width,
                _h = this._height,
                width = _w,
                height = _h,
                _w2 = width << 1,
                _h2 = height << 1;

            // var prevLayerId = -1;

            let extentParamsHigh = new Float32Array(4);
            let extentParamsLow = new Float32Array(4);

            let f = this._framebuffer!.activate();

            let deltaTime = 0,
                startTime = window.performance.now(),
                vertices = 0;

            while (this._queue.length && deltaTime < MAX_FRAME_TIME) {
                let material = this._queue.shift()!;
                const node = material.segment.node;
                if (material.isLoading && (node.getState() !== NOTRENDERING || node.isInFadingTransition())) {
                    let pickingEnabled = material.layer._pickingEnabled;

                    let geomHandler = (material.layer as Vector)._geometryHandler;

                    const picking = pickingEnabled ? 1 : 0,
                        tileVertices =
                            geomHandler._polyIndexesBuffer!.numItems * (1 + picking) +
                            geomHandler._lineIndexesBuffer!.numItems * (4 + picking);

                    if (vertices && vertices + tileVertices > this.maxFrameVertices) {
                        this._queue.unshift(material);
                        break;
                    }

                    vertices += tileVertices;

                    if (material.segment.tileZoom < 4) {
                        width = _w2;
                        height = _h2;
                    } else {
                        width = _w;
                        height = _h;
                    }

                    let texture = material._updateTexture || h.createEmptyTexture_l(width, height);
                    let pickingMask = pickingEnabled
                        ? material._updatePickingMask || h.createEmptyTexture_n(width, height)
                        : null;

                    material.applyTexture(texture, pickingMask);

                    f.setSize(width, height);

                    f.bindOutputTexture(texture!);

                    gl.clearColor(0.0, 0.0, 0.0, 0.0);
                    gl.clear(gl.COLOR_BUFFER_BIT);

                    let extent = material.segment.getExtentMerc();

                    doubleToTwoFloats2(extent.southWest.lon, tempArr);
                    extentParamsHigh[0] = tempArr[0];
                    extentParamsLow[0] = tempArr[1];

                    doubleToTwoFloats2(extent.southWest.lat, tempArr);
                    extentParamsHigh[1] = tempArr[0];
                    extentParamsLow[1] = tempArr[1];

                    extentParamsHigh[2] = 2.0 / extent.getWidth();
                    extentParamsHigh[3] = 2.0 / extent.getHeight();

                    // A cube tile draws the directions of the vertices on its plane
                    const plane = geomHandler._isCube ? this._getTilePlane(material.segment) : null;

                    let sh = plane ? h.programs.vectorTilePolygonRasterizationPlane : hPoly;
                    sh.activate();
                    let sha = sh.attributes,
                        shu = sh.uniforms;

                    //=========================================
                    //Polygon rendering
                    //=========================================
                    if (plane) {
                        this._applyTilePlane(gl, shu, plane);
                    } else {
                        gl.uniform4fv(shu.extentParamsHigh, extentParamsHigh);
                        gl.uniform4fv(shu.extentParamsLow, extentParamsLow);
                    }

                    const polyHigh = plane
                            ? geomHandler._polyDirectionsHighBuffer!
                            : geomHandler._polyVerticesHighBufferMerc!,
                        polyLow = plane
                            ? geomHandler._polyDirectionsLowBuffer!
                            : geomHandler._polyVerticesLowBufferMerc!;

                    gl.bindBuffer(gl.ARRAY_BUFFER, polyHigh);
                    gl.vertexAttribPointer(sha.coordinatesHigh, polyHigh.itemSize, gl.FLOAT, false, 0, 0);

                    gl.bindBuffer(gl.ARRAY_BUFFER, polyLow);
                    gl.vertexAttribPointer(sha.coordinatesLow, polyLow.itemSize, gl.FLOAT, false, 0, 0);

                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._polyColorsBuffer!);
                    gl.vertexAttribPointer(sha.colors, geomHandler._polyColorsBuffer!.itemSize, gl.FLOAT, false, 0, 0);

                    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geomHandler._polyIndexesBuffer!);

                    gl.drawElements(gl.TRIANGLES, geomHandler._polyIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);

                    //Polygon picking PASS
                    if (pickingEnabled) {
                        f.bindOutputTexture(pickingMask!);

                        gl.clearColor(0.0, 0.0, 0.0, 0.0);
                        gl.clear(gl.COLOR_BUFFER_BIT);

                        gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._polyPickingColorsBuffer!);
                        gl.vertexAttribPointer(
                            sha.colors,
                            geomHandler._polyPickingColorsBuffer!.itemSize,
                            gl.FLOAT,
                            false,
                            0,
                            0
                        );

                        gl.drawElements(gl.TRIANGLES, geomHandler._polyIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);
                    }

                    //=========================================
                    //Strokes and linestrings rendering
                    //=========================================
                    f.bindOutputTexture(texture!);

                    sh = plane ? h.programs.vectorTileLineRasterizationPlane : hLine;
                    sh.activate();
                    sha = sh.attributes;
                    shu = sh.uniforms;

                    gl.uniform2fv(shu.viewport, [width, height]);

                    if (plane) {
                        this._applyTilePlane(gl, shu, plane);
                    } else {
                        gl.uniform4fv(shu.extentParamsHigh, extentParamsHigh);
                        gl.uniform4fv(shu.extentParamsLow, extentParamsLow);
                    }

                    //vertex, a point goes four times, so the previous and the next ones are four vertices away
                    let mb = plane ? geomHandler._lineDirectionsHighBuffer! : geomHandler._lineVerticesHighBufferMerc!;
                    const stride = mb.itemSize * 4;
                    gl.bindBuffer(gl.ARRAY_BUFFER, mb);

                    gl.vertexAttribPointer(sha.prevHigh, mb.itemSize, gl.FLOAT, false, stride, 0);
                    gl.vertexAttribPointer(sha.currentHigh, mb.itemSize, gl.FLOAT, false, stride, stride * 4);
                    gl.vertexAttribPointer(sha.nextHigh, mb.itemSize, gl.FLOAT, false, stride, stride * 8);

                    mb = plane ? geomHandler._lineDirectionsLowBuffer! : geomHandler._lineVerticesLowBufferMerc!;
                    gl.bindBuffer(gl.ARRAY_BUFFER, mb);

                    gl.vertexAttribPointer(sha.prevLow, mb.itemSize, gl.FLOAT, false, stride, 0);
                    gl.vertexAttribPointer(sha.currentLow, mb.itemSize, gl.FLOAT, false, stride, stride * 4);
                    gl.vertexAttribPointer(sha.nextLow, mb.itemSize, gl.FLOAT, false, stride, stride * 8);

                    //order
                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._lineOrdersBuffer!);
                    gl.vertexAttribPointer(sha.order, geomHandler._lineOrdersBuffer!.itemSize, gl.FLOAT, false, 4, 0);

                    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geomHandler._lineIndexesBuffer!);

                    //PASS - stroke
                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._lineStrokesBuffer!);
                    gl.vertexAttribPointer(
                        sha.thickness,
                        geomHandler._lineStrokesBuffer!.itemSize,
                        gl.FLOAT,
                        false,
                        0,
                        0
                    );

                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._lineStrokeColorsBuffer!);
                    gl.vertexAttribPointer(
                        sha.color,
                        geomHandler._lineStrokeColorsBuffer!.itemSize,
                        gl.FLOAT,
                        false,
                        0,
                        0
                    );

                    //Antialias pass
                    gl.uniform1f(shu.thicknessOutline, 2);
                    gl.uniform1f(shu.alpha, 0.54);
                    gl.drawElements(gl.TRIANGLE_STRIP, geomHandler._lineIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);
                    //
                    //Aliased pass
                    gl.uniform1f(shu.thicknessOutline, 1);
                    gl.uniform1f(shu.alpha, 1.0);
                    gl.drawElements(gl.TRIANGLE_STRIP, geomHandler._lineIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);

                    //PASS - inside line
                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._lineThicknessBuffer!);
                    gl.vertexAttribPointer(
                        sha.thickness,
                        geomHandler._lineThicknessBuffer!.itemSize,
                        gl.FLOAT,
                        false,
                        0,
                        0
                    );

                    gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._lineColorsBuffer!);
                    gl.vertexAttribPointer(sha.color, geomHandler._lineColorsBuffer!.itemSize, gl.FLOAT, false, 0, 0);

                    //Antialias pass
                    gl.uniform1f(shu.thicknessOutline, 2);
                    gl.uniform1f(shu.alpha, 0.54);
                    gl.drawElements(gl.TRIANGLE_STRIP, geomHandler._lineIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);
                    //
                    //Aliased pass
                    gl.uniform1f(shu.thicknessOutline, 1);
                    gl.uniform1f(shu.alpha, 1.0);
                    gl.drawElements(gl.TRIANGLE_STRIP, geomHandler._lineIndexesBuffer!.numItems, gl.UNSIGNED_INT, 0);

                    if (pickingEnabled) {
                        f.bindOutputTexture(pickingMask!);
                        gl.uniform1f(shu.thicknessOutline, 8);
                        gl.bindBuffer(gl.ARRAY_BUFFER, geomHandler._linePickingColorsBuffer!);
                        gl.vertexAttribPointer(
                            sha.color,
                            geomHandler._linePickingColorsBuffer!.itemSize,
                            gl.FLOAT,
                            false,
                            0,
                            0
                        );
                        gl.drawElements(
                            gl.TRIANGLE_STRIP,
                            geomHandler._lineIndexesBuffer!.numItems,
                            gl.UNSIGNED_INT,
                            0
                        );
                    }
                } else {
                    material.isLoading = false;
                }

                deltaTime = window.performance.now() - startTime;
            }

            gl.enable(gl.DEPTH_TEST);
            gl.enable(gl.CULL_FACE);

            f.deactivate();
        }
    }

    /**
     * Returns the plane of a cube tile: its face plane with the tile center on it, or null for a tile of another
     * projection.
     * @protected
     */
    protected _getTilePlane(segment: Segment): typeof _plane | null {
        if (!isCubeProj(segment._projection)) {
            return null;
        }

        const proj = segment._projection as CubeProj;

        const face = segment.tileGroup,
            f = CUBE_FACES[face],
            e = segment.getExtent(),
            s0 = e.southWest.lon - face,
            s1 = e.northEast.lon - face,
            t0 = e.southWest.lat,
            t1 = e.northEast.lat,
            u = proj.toPlane((s0 + s1) * 0.5),
            v = proj.toPlane((t0 + t1) * 0.5);

        const x = f[0] + u * f[3] + v * f[6],
            y = f[1] + u * f[4] + v * f[7],
            z = f[2] + u * f[5] + v * f[8],
            l = Math.sqrt(x * x + y * y + z * z),
            c = [x / l, y / l, z / l];

        for (let i = 0; i < 3; i++) {
            _plane.centerHigh[i] = c[i];
            _plane.centerLow[i] = c[i] - _plane.centerHigh[i];
            _plane.axisC[i] = f[i];
            _plane.axisU[i] = f[3 + i];
            _plane.axisV[i] = f[6 + i];
        }

        // Plane coordinates are twice the position for the common cube, the angles are a quarter of pi for it on
        // the equiangular one
        const k = proj.equiangular ? 4.0 / Math.PI : 1.0;

        _plane.center[0] = u;
        _plane.center[1] = v;
        _plane.scale[0] = k / (s1 - s0);
        _plane.scale[1] = k / (t1 - t0);
        _plane.equiangular = proj.equiangular ? 1 : 0;

        return _plane;
    }

    protected _applyTilePlane(gl: WebGL2RenderingContext, shu: Record<string, any>, plane: typeof _plane) {
        gl.uniform3fv(shu.centerHigh, plane.centerHigh);
        gl.uniform3fv(shu.centerLow, plane.centerLow);
        gl.uniform3fv(shu.axisU, plane.axisU);
        gl.uniform3fv(shu.axisV, plane.axisV);
        gl.uniform3fv(shu.axisC, plane.axisC);
        gl.uniform2fv(shu.center, plane.center);
        gl.uniform2fv(shu.scale, plane.scale);
        gl.uniform1f(shu.equiangular, plane.equiangular);
    }

    public add(material: Material) {
        this._queue.push(material);
    }

    public remove(material: Material) {
        //...
    }

    public get queueSize(): number {
        return this._queue.length;
    }
}