Home Reference Source

cables_dev/cables_ui/src/ui/gldraw/glrectinstancer.js

import { Logger, Events } from "cables-shared-client";
import GlRect from "./glrect.js";
import srcShaderGlRectInstancerFrag from "./glrectinstancer_glsl.frag";
import srcShaderGlRectInstancerVert from "./glrectinstancer_glsl.vert";

/**
 * draw many rectangles quickly using GPU instancing (e.g. patchfield: ops,ports,text)
 *
 * @export
 * @class GlRectInstancer
 * @extends {Events}
 */
export default class GlRectInstancer extends Events
{
    constructor(cgl, options)
    {
        super();
        options = options || {};
        this._log = new Logger("glrectinstancer");

        if (!cgl)
        {
            this._log.warn("[RectInstancer] no cgl");
            throw new Error("[RectInstancer] no cgl");
        }

        this._name = options.name || "unknown";

        this._debugRenderStyle = 0;
        this.doBulkUploads = true;

        this._DEFAULT_BIGNUM = 999999;

        this._counter = 0;
        this._num = options.initNum || 5000;
        this._needsRebuild = true;
        this._needsRebuildReason = "";
        this._rects = [];
        this._textures = [];
        this._interactive = true;
        this.allowDragging = false;
        this._cgl = cgl;
        this._needsTextureUpdate = false;
        this._draggingRect = null;
        this._reUploadAttribs = true;
        this._updateRangesMin = {};
        this._updateRangesMax = {};
        this._bounds = { "minX": this._DEFAULT_BIGNUM, "maxX": -this._DEFAULT_BIGNUM, "minY": this._DEFAULT_BIGNUM, "maxY": -this._DEFAULT_MBIGNUM9, "minZ": this._DEFAULT_BIGNUM, "maxZ": -this._DEFAULT_BIGNUM };

        this._meshAttrPos = null;
        this._meshAttrCol = null;
        this._meshAttrSize = null;
        this._meshAttrDeco = null;
        this._meshAttrTexRect = null;
        this._meshAttrTex = null;


        this._setupAttribBuffers();

        this.ATTR_TEXRECT = "texRect";
        this.ATTR_CONTENT_TEX = "contentTexture";
        this.ATTR_POS = "instPos";
        this.ATTR_COLOR = "instCol";
        this.ATTR_SIZE = "instSize";
        this.ATTR_DECO = "instDeco";

        this._shader = new CGL.Shader(cgl, "rectinstancer " + this._name);
        this._shader.setSource(srcShaderGlRectInstancerVert, srcShaderGlRectInstancerFrag);
        this._shader.ignoreMissingUniforms = true;

        this._uniTime = new CGL.Uniform(this._shader, "f", "time", 0);
        this._uniZoom = new CGL.Uniform(this._shader, "f", "zoom", 0);
        this._uniResX = new CGL.Uniform(this._shader, "f", "resX", 0);
        this._uniResY = new CGL.Uniform(this._shader, "f", "resY", 0);
        this._uniscrollX = new CGL.Uniform(this._shader, "f", "scrollX", 0);
        this._uniscrollY = new CGL.Uniform(this._shader, "f", "scrollY", 0);
        this._unimsdfUnit = new CGL.Uniform(this._shader, "f", "msdfUnit", 8 / 1024);
        this._uniTexture = new CGL.Uniform(this._shader, "t", "tex", 0);

        this._geom = new CGL.Geometry("rectinstancer " + this._name);
        this._geom.vertices = new Float32Array([1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]);
        this._geom.verticesIndices = new Uint16Array([2, 1, 0, 3, 1, 2]);
        this._geom.texCoords = new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]);

        if (this._cgl.glVersion == 1) this._shader.enableExtension("GL_OES_standard_derivatives");
        this._mesh = new CGL.Mesh(cgl, this._geom);
        this._mesh.numInstances = this._num;

        this.clear();
    }

    set interactive(i) { this._interactive = i; }

    get interactive() { return this._interactive; }

    dispose()
    {
    }

    get bounds()
    {
        if (this._needsBoundsRecalc)
        {
            const perf = CABLES.UI.uiProfiler.start("[glRectInstancer] recalcBounds");

            const defaultMin = this._DEFAULT_BIGNUM;
            const defaultMax = -this._DEFAULT_BIGNUM;
            this._newBounds = { "minX": defaultMin, "maxX": defaultMax, "minY": defaultMin, "maxY": defaultMax, "minZ": defaultMin, "maxZ": defaultMax };
            for (let i = 0; i < this._rects.length; i++)
            {
                if (!this._rects[i].visible) continue;
                if (this._rects[i].x == this._bounds.minX && this._rects[i].y == this._bounds.minY && this._rects[i].w == this._bounds.maxX - this._bounds.minX && this._rects[i].h == this._bounds.maxY - this._bounds.minY) continue;

                const x = this._rects[i].x || 0;
                const y = this._rects[i].y || 0;
                const z = this._rects[i].z || 0;
                const x2 = x + this._rects[i].w;
                const y2 = y + this._rects[i].h;

                this._newBounds.minX = Math.min(x, this._newBounds.minX);
                this._newBounds.maxX = Math.max(x2, this._newBounds.maxX);
                this._newBounds.minY = Math.min(y, this._newBounds.minY);
                this._newBounds.maxY = Math.max(y2, this._newBounds.maxY);

                this._newBounds.minZ = Math.min(z, this._newBounds.minZ);
                this._newBounds.maxZ = Math.max(z, this._newBounds.maxZ);
            }

            this._newBounds.changed = (this._newBounds.minX != defaultMin || this._newBounds.minY != defaultMin);
            this._needsBoundsRecalc = false;
            perf.finish();
        }

        this._bounds = this._newBounds;
        return this._bounds;
    }

    getDebug()
    {
        return {
            "num": this._num,
            "len_attrBuffSizes": this._attrBuffSizes.length,
            "len_attrBuffPos": this._attrBuffPos.length,
            "len_attrBuffCol": this._attrBuffCol.length,
            "len_attrBuffDeco": this._attrBuffDeco.length
        };
    }

    clear()
    {
        for (let i = 0; i < 2 * this._num; i++) this._attrBuffSizes[i] = 0;// Math.random()*61;
        for (let i = 0; i < 3 * this._num; i++) this._attrBuffPos[i] = 0;// Math.random()*60;
        for (let i = 0; i < 4 * this._num; i++) this._attrBuffCol[i] = 1;// Math.random();
        for (let i = 0; i < 4 * this._num; i++) this._attrBuffDeco[i] = 0;// Math.random();
        for (let i = 0; i < this._num; i++) this._attrBuffTextures[i] = -1;// Math.random();

        for (let i = 0; i < 4 * this._num; i += 4)
        {
            this._attrBuffTexRect[i + 0] = this._attrBuffTexRect[i + 1] = 0;
            this._attrBuffTexRect[i + 2] = this._attrBuffTexRect[i + 3] = 1;
        }
    }

    _setupAttribBuffers()
    {
        const oldAttrPositions = this._attrBuffPos;
        const oldAttrTextures = this._attrBuffTextures;
        const oldAttrColors = this._attrBuffCol;
        const oldAttrSizes = this._attrBuffSizes;
        const oldAttrDeco = this._attrBuffDeco;
        const oldAttrTexRect = this._attrBuffTexRect;

        this._attrBuffPos = new Float32Array(3 * this._num);
        this._attrBuffTextures = new Float32Array(this._num);
        this._attrBuffCol = new Float32Array(4 * this._num);
        this._attrBuffSizes = new Float32Array(2 * this._num);
        this._attrBuffDeco = new Float32Array(4 * this._num);
        this._attrBuffTexRect = new Float32Array(4 * this._num);
        this.clear();

        if (oldAttrPositions) this._attrBuffPos.set(oldAttrPositions);
        if (oldAttrTextures) this._attrBuffTextures.set(oldAttrTextures);
        if (oldAttrColors) this._attrBuffCol.set(oldAttrColors);
        if (oldAttrSizes) this._attrBuffSizes.set(oldAttrSizes);
        if (oldAttrDeco) this._attrBuffDeco.set(oldAttrDeco);
        if (oldAttrTexRect) this._attrBuffTexRect.set(oldAttrTexRect);
    }

    isDragging()
    {
        return this._draggingRect != null;
    }

    _setupTextures()
    {
        this._needsTextureUpdate = false;
        this._textures.length = 0;
        let count = 0;

        let minIdx = this._DEFAULT_BIGNUM;
        let maxIdx = -this._DEFAULT_BIGNUM;


        for (let i = 0; i < this._rects.length; i++)
        {
            let changed = false;
            const thatRectIdx = this._rects[i].idx;

            if (this._rects[i].texture)
            {
                let found = false;


                for (let j = 0; j < this._textures.length; j++)
                {
                    if (this._textures[j] && this._textures[j].texture == this._rects[i].texture)
                    {
                        found = true;

                        if (this._attrBuffTextures[thatRectIdx] != this._textures[j].num)changed = true;

                        this._attrBuffTextures[thatRectIdx] = this._textures[j].num;
                        minIdx = Math.min(thatRectIdx, minIdx);
                        maxIdx = Math.max(thatRectIdx, maxIdx);
                    }
                }

                if (!found)
                {
                    this._attrBuffTextures[thatRectIdx] = count;
                    this._textures[count] =
                        {
                            "texture": this._rects[i].texture,
                            "num": count
                        };
                    count++;
                }
            }
            else
            {
                if (this._attrBuffTextures[thatRectIdx] != -1) changed = true;
                this._attrBuffTextures[thatRectIdx] = -1;
            }

            if (changed)
            {
                minIdx = Math.min(this._rects[i].idx, minIdx);
                maxIdx = Math.max(this._rects[i].idx, maxIdx);
            }
        }

        this._mesh.setAttributeRange(this._meshAttrTex, this._attrBuffCol, minIdx, maxIdx);
    }

    _bindTextures()
    {
        for (let i = 0; i < 4; i++)
            if (this._textures[0])
                this._cgl.setTexture(i, this._textures[0].texture.tex);

        if (this._textures[0]) this._cgl.setTexture(0, this._textures[0].texture.tex);
    }

    render(resX, resY, scrollX, scrollY, zoom)
    {
        // console.log(zoom);
        if (zoom > 500 && zoom < 800)
        {
        }
        // else gui.patchView._patchRenderer._textWriter._rectDrawer._unimsdfUnit.setValue(0);
        gui.patchView._patchRenderer._textWriter._rectDrawer._unimsdfUnit.setValue(8 / zoom);
        // else if (zoom > 800 && zoom < 1100)
        // {
        //     console.log(2);
        //     gui.patchView._patchRenderer._textWriter._rectDrawer._unimsdfUnit.setValue(8 / 450);
        // }
        // else if (zoom > 1100)
        // {
        //     console.log(3);
        //     gui.patchView._patchRenderer._textWriter._rectDrawer._unimsdfUnit.setValue(8 / 250);
        // }
        // else gui.patchView._patchRenderer._textWriter._rectDrawer._unimsdfUnit.setValue(8 / 1000);

        if (this.doBulkUploads)
        {
            if (this._updateRangesMin[this.ATTR_POS] != this._DEFAULT_BIGNUM)
            {
                this._mesh.setAttributeRange(this._meshAttrPos, this._attrBuffPos, this._updateRangesMin[this.ATTR_POS], this._updateRangesMax[this.ATTR_POS]);
                this._resetAttrRange(this.ATTR_POS);
            }

            if (this._updateRangesMin[this.ATTR_COLOR] != this._DEFAULT_BIGNUM)
            {
                // console.log("update colors,", this._updateRangesMax[this.ATTR_COLOR] - this._updateRangesMin[this.ATTR_COLOR]);
                this._mesh.setAttributeRange(this._meshAttrCol, this._attrBuffCol, this._updateRangesMin[this.ATTR_COLOR], this._updateRangesMax[this.ATTR_COLOR]);
                this._resetAttrRange(this.ATTR_COLOR);
            }

            if (this._updateRangesMin[this.ATTR_SIZE] != this._DEFAULT_BIGNUM)
            {
                this._mesh.setAttributeRange(this._meshAttrSize, this._attrBuffSizes, this._updateRangesMin[this.ATTR_SIZE], this._updateRangesMax[this.ATTR_SIZE]);
                this._resetAttrRange(this.ATTR_SIZE);
            }

            if (this._updateRangesMin[this.ATTR_DECO] != this._DEFAULT_BIGNUM)
            {
                this._mesh.setAttributeRange(this._meshAttrDeco, this._attrBuffDeco, this._updateRangesMin[this.ATTR_DECO], this._updateRangesMax[this.ATTR_DECO]);
                this._resetAttrRange(this.ATTR_DECO);
            }

            if (this._updateRangesMin[this.ATTR_TEXRECT] != this._DEFAULT_BIGNUM)
            {
                this._mesh.setAttributeRange(this._meshAttrTexRect, this._attrBuffTexRect, this._updateRangesMin[this.ATTR_TEXRECT], this._updateRangesMax[this.ATTR_TEXRECT]);
                this._resetAttrRange(this.ATTR_TEXRECT);
            }
        }

        this._uniResX.set(resX);
        this._uniResY.set(resY);
        this._uniscrollX.set(scrollX);
        this._uniscrollY.set(scrollY);
        this._uniZoom.set(1.0 / zoom);


        this._uniTime.set(performance.now() / 1000);

        if (this._needsTextureUpdate) this._setupTextures();
        this._bindTextures();

        if (this._needsRebuild) this.rebuild();

        this.emitEvent("render");

        this._mesh.render(this._shader);
    }

    rebuild()
    {
        // this._log.log("rebuild!", this._name, this._attrBuffPos.length / 3, this._needsRebuildReason);
        this._needsRebuildReason = "";
        // todo only update whats needed

        this._mesh.numInstances = this._num;

        if (this._reUploadAttribs)
        {
            const perf = CABLES.UI.uiProfiler.start("[glRectInstancer] _reUploadAttribs");
            this._meshAttrPos = this._mesh.setAttribute(this.ATTR_POS, this._attrBuffPos, 3, { "instanced": true });
            this._meshAttrCol = this._mesh.setAttribute(this.ATTR_COLOR, this._attrBuffCol, 4, { "instanced": true });
            this._meshAttrSize = this._mesh.setAttribute(this.ATTR_SIZE, this._attrBuffSizes, 2, { "instanced": true });
            this._meshAttrDeco = this._mesh.setAttribute(this.ATTR_DECO, this._attrBuffDeco, 4, { "instanced": true });
            this._meshAttrTexRect = this._mesh.setAttribute(this.ATTR_TEXRECT, this._attrBuffTexRect, 4, { "instanced": true });
            this._meshAttrTex = this._mesh.setAttribute(this.ATTR_CONTENT_TEX, this._attrBuffTextures, 1, { "instanced": true });
            this._reUploadAttribs = false;
            perf.finish();
        }

        this._needsRebuild = false;
    }

    getNumRects()
    {
        return this._counter;
    }

    getIndex()
    {
        this._counter++;
        if (this._counter > this._num - 100)
        {
            this._num += Math.max(5000, Math.ceil(this._num));
            this._setupAttribBuffers();
            this._needsRebuild = true;
            this._needsRebuildReason = "resize";
            this._needsTextureUpdate = true;
            this._reUploadAttribs = true;
        }
        return this._counter;
    }

    _float32Diff(a, b)
    {
        return Math.abs(a - b) > 0.0001;
    }

    setPosition(idx, x, y, z)
    {
        const buffIdx = idx * 3;
        if (this._float32Diff(this._attrBuffPos[buffIdx + 0], x) || this._float32Diff(this._attrBuffPos[buffIdx + 1], y) || this._float32Diff(this._attrBuffPos[buffIdx + 2], z))
        {
            // this._needsRebuild = true;
            // this._needsRebuildReason = "pos change";
        }
        else return;

        if (
            this._attrBuffPos[buffIdx + 0] >= this._bounds.maxX || this._attrBuffPos[buffIdx + 0] <= this._bounds.minX ||
            this._attrBuffPos[buffIdx + 1] >= this._bounds.maxY || this._attrBuffPos[buffIdx + 1] <= this._bounds.minY)
        {
            this._needsBoundsRecalc = true;
        }

        this._attrBuffPos[buffIdx + 0] = x;
        this._attrBuffPos[buffIdx + 1] = y;
        this._attrBuffPos[buffIdx + 2] = z;

        if (
            this._attrBuffPos[buffIdx + 0] >= this._bounds.maxX || this._attrBuffPos[buffIdx + 0] <= this._bounds.minX ||
            this._attrBuffPos[buffIdx + 1] >= this._bounds.maxY || this._attrBuffPos[buffIdx + 1] <= this._bounds.minY)
        {
            this._needsBoundsRecalc = true;
        }

        if (!this._needsBoundsRecalc)
        {
            this._bounds.minX = Math.min(this._attrBuffPos[buffIdx + 0], this._bounds.minX);
            this._bounds.maxX = Math.max(this._attrBuffPos[buffIdx + 0], this._bounds.maxX);

            this._bounds.minY = Math.min(this._attrBuffPos[buffIdx + 1], this._bounds.minY);
            this._bounds.maxY = Math.max(this._attrBuffPos[buffIdx + 1], this._bounds.maxY);

            this._bounds.minZ = Math.min(this._attrBuffPos[buffIdx + 2], this._bounds.minZ);
            this._bounds.maxZ = Math.max(this._attrBuffPos[buffIdx + 2], this._bounds.maxZ);
        }

        if (this.doBulkUploads) this._setAttrRange(this.ATTR_POS, buffIdx, buffIdx + 3);
        else this._mesh.setAttributeRange(this._meshAttrPos, this._attrBuffPos, buffIdx, buffIdx + 3);

        this._needsBoundsRecalc = true;
    }

    setSize(idx, x, y)
    {
        if (this._float32Diff(this._attrBuffSizes[idx * 2 + 0], x) || this._float32Diff(this._attrBuffSizes[idx * 2 + 1], y))
        {
            // this._needsRebuild = true;
            // this._needsRebuildReason = "size change";
        }
        else return;

        this._attrBuffSizes[idx * 2 + 0] = x;
        this._attrBuffSizes[idx * 2 + 1] = y;

        if (this.doBulkUploads) this._setAttrRange(this.ATTR_SIZE, idx * 2, (idx + 1) * 2);
        else this._mesh.setAttributeRange(this._meshAttrSize, this._attrBuffSizes, idx * 2, (idx + 1) * 2);
    }

    setTexRect(idx, x, y, w, h)
    {
        if (
            this._float32Diff(this._attrBuffTexRect[idx * 4 + 0], x) ||
            this._float32Diff(this._attrBuffTexRect[idx * 4 + 1], y) ||
            this._float32Diff(this._attrBuffTexRect[idx * 4 + 2], w) ||
            this._float32Diff(this._attrBuffTexRect[idx * 4 + 3], h))
        {
            // this._needsRebuild = true;
            // this._needsRebuildReason = "texrect";
        }
        else return;

        this._attrBuffTexRect[idx * 4 + 0] = x;
        this._attrBuffTexRect[idx * 4 + 1] = y;
        this._attrBuffTexRect[idx * 4 + 2] = w;
        this._attrBuffTexRect[idx * 4 + 3] = h;

        if (this.doBulkUploads) this._setAttrRange(this.ATTR_TEXRECT, idx * 4, idx * 4 + 4);
        else this._mesh.setAttributeRange(this._meshAttrTexRect, this._attrBuffTexRect, idx * 4, idx * 4 + 4);
    }

    setColor(idx, r, g, b, a)
    {
        if (r.length)
        {
            a = r[3];
            b = r[2];
            g = r[1];
            r = r[0];
        }
        if (
            this._float32Diff(this._attrBuffCol[idx * 4 + 0], r) ||
            this._float32Diff(this._attrBuffCol[idx * 4 + 1], g) ||
            this._float32Diff(this._attrBuffCol[idx * 4 + 2], b) ||
            this._float32Diff(this._attrBuffCol[idx * 4 + 3], a))
        {
            // this._needsRebuild = true;
            // this._needsRebuildReason = "setcolor";
            // this._setAttrRange(this._meshAttrCol, idx * 4, idx * 4 + 4);
        }
        else return;

        this._attrBuffCol[idx * 4 + 0] = r;
        this._attrBuffCol[idx * 4 + 1] = g;
        this._attrBuffCol[idx * 4 + 2] = b;
        this._attrBuffCol[idx * 4 + 3] = a;

        if (this.doBulkUploads) this._setAttrRange(this.ATTR_COLOR, idx * 4, idx * 4 + 4);
        else this._mesh.setAttributeRange(this._meshAttrCol, this._attrBuffCol, idx * 4, idx * 4 + 4);
    }

    setShape(idx, o)
    {
        this._attrBuffDeco[idx * 4 + 0] = o;
        if (this.doBulkUploads) this._setAttrRange(this.ATTR_DECO, idx * 4, idx * 4 + 4);
        else this._mesh.setAttributeRange(this._meshAttrDeco, this._attrBuffDeco, idx * 4, idx * 4 + 4);
    }

    setBorder(idx, o)
    {
        this._attrBuffDeco[idx * 4 + 1] = o;
        if (this.doBulkUploads) this._setAttrRange(this.ATTR_DECO, idx * 4, idx * 4 + 4);
        else this._mesh.setAttributeRange(this._meshAttrDeco, this._attrBuffDeco, idx * 4, idx * 4 + 4);
    }

    setSelected(idx, o)
    {
        this._attrBuffDeco[idx * 4 + 2] = o;

        if (this.doBulkUploads) this._setAttrRange(this.ATTR_DECO, idx * 4, idx * 4 + 4);
        else this._mesh.setAttributeRange(this._meshAttrDeco, this._attrBuffDeco, idx * 4, idx * 4 + 4);
    }

    setDebugRenderer(i)
    {
        this._shader.toggleDefine("DEBUG_1", i == 1);
        this._shader.toggleDefine("DEBUG_2", i == 2);
    }

    setAllTexture(tex, sdf)
    {
        this._shader.toggleDefine("SDF_TEXTURE", sdf);

        for (let i = 0; i < this._rects.length; i++)
            this._rects[i].setTexture(tex);
    }

    _resetAttrRange(attr)
    {
        this._updateRangesMin[attr] = this._DEFAULT_BIGNUM;
        this._updateRangesMax[attr] = -this._DEFAULT_BIGNUM;
    }

    _setAttrRange(attr, start, end)
    {
        this._updateRangesMin[attr] = Math.min(start, this._updateRangesMin[attr]);
        this._updateRangesMax[attr] = Math.max(end, this._updateRangesMax[attr]);
    }

    // setOutline(idx,o)
    // {
    //     if(this._attrOutline[idx]!=o) { this._needsRebuild=true; }
    //     this._attrOutline[idx]=o;
    // }

    createRect(options)
    {
        options = options || {};
        const r = new GlRect(this, options);
        this._rects.push(r);

        if (options.draggable)
        {
            this.allowDragging = options.draggable;
            r.on("dragStart", (rect) => { if (this.allowDragging) this._draggingRect = rect; });
            r.on("dragEnd", () => { this._draggingRect = null; });
        }

        r.on("textureChanged", () => { this._needsTextureUpdate = true; });

        return r;
    }

    mouseMove(x, y, button, event)
    {
        const perf = CABLES.UI.uiProfiler.start("[glrectinstancer] mousemove");
        if (!this._interactive) return;
        if (this.allowDragging && this._draggingRect)
        {
            this._draggingRect.mouseDrag(x, y, button);
            return;
        }

        for (let i = 0; i < this._rects.length; i++)
            if (!this._rects[i].parent)
                this._rects[i].mouseMove(x, y, button);

        perf.finish();
    }

    mouseDown(e)
    {
        if (!this._interactive) return;

        const perf = CABLES.UI.uiProfiler.start("[glrectinstancer] mouseDown");
        for (let i = 0; i < this._rects.length; i++)
            if (!this._rects[i].parent)
                this._rects[i].mouseDown(e);
        perf.finish();
    }

    mouseUp(e)
    {
        if (!this._interactive) return;
        const perf = CABLES.UI.uiProfiler.start("[glrectinstancer] mouseup");

        for (let i = 0; i < this._rects.length; i++) this._rects[i].mouseUp(e);
        perf.finish();

        if (this._draggingRect) this._draggingRect.mouseDragEnd();
    }
}