UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

827 lines (826 loc) 30.2 kB
import buildMask from './buildMask.js'; const DEPTH_ESPILON = 1 / Math.pow(2, 16); // CONSIDER: get apple devices https://github.com/pmndrs/detect-gpu/blob/master/src/internal/deobfuscateAppleGPU.ts /** * # Context * * ## Description * A WebGL(1|2) context with GPU information. * A useful wrapper to store state and reduce costly GPU calls when unnecessary */ export default class Context { gl; painter; type = 1; projection = 'S2'; presentation = { width: 0, height: 0 }; renderer; // ex: AMD Radeon Pro 560 OpenGL Engine (https://github.com/pmndrs/detect-gpu) devicePixelRatio; interactive = false; depthState; cullState; stencilState; blendState; stencilRef = -1; blendMode = -1; // 0 -> default ; 1 -> zTestMode = -1; // 0 -> always ; 1 -> less ; 2 -> lessThenOrEqual zLow = 0; zHigh = 1; currWorkflow = undefined; clearColorRGBA = [0, 0, 0, 0]; featurePoint = new Uint8Array(4); masks = new Map(); // <zoom, mask> vao; vertexBuffer; interactTexture; stencilBuffer; interactFramebuffer; defaultBounds = [0, 0, 1, 1]; nullTexture; sharedFBO; /** * @param context - The WebGL1 or WebGL2 context to read from * @param options - Map options * @param painter - The painter that will use this context to manage rendering state */ constructor(context, options, painter) { const { canvasMultiplier } = options; const gl = (this.gl = context); this.painter = painter; this.devicePixelRatio = canvasMultiplier ?? 1; this.#buildNullTexture(); this.sharedFBO = this.#buildFramebuffer(200); this.#buildInteractFBO(); // lastly grab the renderers id const debugRendererInfo = context.getExtension('WEBGL_debug_renderer_info'); if (debugRendererInfo !== null) this.renderer = cleanRenderer(context.getParameter(debugRendererInfo.UNMASKED_RENDERER_WEBGL)); else this.renderer = context.getParameter(context.RENDERER); // set initial states gl.enable(gl.STENCIL_TEST); this.stencilState = true; gl.enable(gl.DEPTH_TEST); this.depthState = true; gl.enable(gl.CULL_FACE); this.cullState = true; gl.enable(gl.BLEND); this.blendState = true; this.defaultBlend(); } /* SETUP NULL TEXTURE */ /** Setup a null texture for cases where we don't need to use the texture but the uniform is required */ #buildNullTexture() { this.nullTexture = this.buildTexture(null, 1); } /* MANAGE FRAMEBUFFER OBJECTS */ /** * Setup a framebuffer for things like glyph/icon/sprite/image rendering * @param height - The height of the framebuffer * @returns A framebuffer that can handle glyph/icon/sprite/image rendering */ #buildFramebuffer(height) { const { gl } = this; const texture = gl.createTexture(); if (texture === null) throw new Error('Failed to create glyph texture'); const stencil = gl.createRenderbuffer(); if (stencil === null) throw new Error('Failed to create glyph stencil'); const glyphFramebuffer = gl.createFramebuffer(); if (glyphFramebuffer === null) throw new Error('Failed to create glyph framebuffer'); // TEXTURE BUFFER // pre-build the glyph texture // bind gl.bindTexture(gl.TEXTURE_2D, texture); // allocate size gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2048, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // set filter system gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // DEPTH & STENCIL BUFFER // bind gl.bindRenderbuffer(gl.RENDERBUFFER, stencil); // allocate size gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, 2048, height); // FRAMEBUFFER // bind gl.bindFramebuffer(gl.FRAMEBUFFER, glyphFramebuffer); // attach texture to glyphFramebuffer gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); // attach stencil renderbuffer to framebuffer gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil); // rebind our default framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, null); return { width: 2048, height, texSize: [2048, height], texture, stencil, glyphFramebuffer, }; } /** * Increase the size of the glyph/icon/sprite/image framebuffer to accomodate more data * @param height - The height of the framebuffer */ #increaseFBOSize(height) { const { gl, type, sharedFBO } = this; if (height <= sharedFBO.height) return; // build the new fbo const newFBO = this.#buildFramebuffer(height); // copy over data if (type === 1) { gl.bindFramebuffer(gl.FRAMEBUFFER, sharedFBO.glyphFramebuffer); gl.bindTexture(gl.TEXTURE_2D, newFBO.texture); gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, 2048, sharedFBO.height); } else { const gl2 = gl; gl2.bindFramebuffer(gl2.READ_FRAMEBUFFER, sharedFBO.glyphFramebuffer); gl2.bindFramebuffer(gl2.DRAW_FRAMEBUFFER, newFBO.glyphFramebuffer); gl2.blitFramebuffer(0, 0, 2048, sharedFBO.height, 0, 0, 2048, sharedFBO.height, gl.COLOR_BUFFER_BIT, gl.LINEAR); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); // delete old FBO and set new this.#deleteFBO(sharedFBO); // update to new FBO this.sharedFBO = newFBO; } /** * Delete a framebuffer * @param fbo - The framebuffer to cleanup */ #deleteFBO(fbo) { const { gl } = this; if (fbo !== undefined) { gl.deleteTexture(fbo.texture); gl.deleteRenderbuffer(fbo.stencil); gl.deleteFramebuffer(fbo.glyphFramebuffer); } } /* MANAGE IMAGE IMPORTS */ /** * Inject a glyph/icon image to the GPU * @param maxHeight - the maximum height of the texture required to hold the image * @param images - the glyph/icon images */ injectImages(maxHeight, images) { const { gl } = this; // increase texture size if necessary this.#increaseFBOSize(maxHeight); // iterate through images and store gl.bindTexture(gl.TEXTURE_2D, this.sharedFBO.texture); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); for (const { posX, posY, width, height, data } of images) { const srcData = new Uint8ClampedArray(data); gl.texSubImage2D(gl.TEXTURE_2D, 0, posX, posY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, srcData, 0); } } /** * Inject a sprite image to the GPU * @param data - the raw image data of the sprite */ injectSpriteImage(data) { const { gl } = this; const { image, offsetX, offsetY, width, height, maxHeight } = data; // increase texture size if necessary this.#increaseFBOSize(maxHeight); // do not premultiply gl.bindTexture(gl.TEXTURE_2D, this.sharedFBO.texture); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0); // (target: number, level: number, xoffset: number, yoffset: number, width: number, height: number, format: number, type: number, source: ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement) => void) & if (this.type === 1) gl.texSubImage2D(gl.TEXTURE_2D, 0, offsetX, offsetY, gl.RGBA, gl.UNSIGNED_BYTE, image); else gl.texSubImage2D(gl.TEXTURE_2D, 0, offsetX, offsetY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, image); } /* SETUP INTERACTIVE BUFFER */ /** Setup an interactive FBO */ #buildInteractFBO() { const { gl } = this; // TEXTURE & STENCIL const texture = gl.createTexture(); if (texture === null) throw new Error('Failed to create interactive texture'); this.interactTexture = texture; const stencil = gl.createRenderbuffer(); if (stencil === null) throw new Error('Failed to create interactive stencil buffer'); this.stencilBuffer = stencil; this.resizeInteract(); // FRAMEBUBFFER const interactFrameBuffer = gl.createFramebuffer(); if (interactFrameBuffer === null) throw new Error('Failed to create interactive framebuffer'); this.interactFramebuffer = interactFrameBuffer; gl.bindFramebuffer(gl.FRAMEBUFFER, interactFrameBuffer); // attach texture to feature framebuffer gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); // attach stencilBuffer renderbuffer to feature framebuffer gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil); // cleanup gl.bindFramebuffer(gl.FRAMEBUFFER, null); } /** Resize the size of the canvas and all associating buffers */ resize() { const { width, height } = this.gl.canvas; this.presentation = { width, height }; this.resizeInteract(); } /** * Set the interactive mode * @param interactive - the interactive mode (true means it is interactive) */ setInteractive(interactive) { this.interactive = interactive; this.resizeInteract(); } /** * Set the projection type (S2 or WM) * @param projection - the projection */ setProjection(projection) { const { gl } = this; this.projection = projection; if (projection === 'S2') gl.cullFace(gl.BACK); else gl.cullFace(gl.FRONT); } /** Resize the interactive buffer */ resizeInteract() { const { gl, interactive } = this; const width = interactive ? gl.canvas.width : 1; const height = interactive ? gl.canvas.height : 1; // bind the pointTexture gl.bindTexture(gl.TEXTURE_2D, this.interactTexture); // update the texture's aspect gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // set filter system gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // update the depthBuffer's aspect gl.bindRenderbuffer(gl.RENDERBUFFER, this.stencilBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, width, height); } /** * Get the collection of features found at the mouse position * @param x - x mouse position * @param y - y mouse position * @returns the collection of features found */ async getFeatureAtMousePosition(x, y) { const { gl, interactFramebuffer, featurePoint } = this; const res = []; // bind the feature framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, interactFramebuffer); // grab the data gl.readPixels(x, gl.canvas.height - y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, featurePoint); if (featurePoint[3] !== 255) return res; // create the actual feature id const featureID = featurePoint[0] + (featurePoint[1] << 8) + (featurePoint[2] << 16); // return if we found something if (featureID > 0) res.push(featureID); return await res; } /** Delete/cleanup the context */ delete() { const { gl, vertexBuffer, vao, interactTexture, stencilBuffer, interactFramebuffer } = this; // remove local data gl.deleteBuffer(vertexBuffer); gl.deleteVertexArray(vao); gl.deleteTexture(interactTexture); gl.deleteRenderbuffer(stencilBuffer); gl.deleteFramebuffer(interactFramebuffer); // remove all possible references // gl.bindBuffer(gl.ARRAY_BUFFER, null) // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null) // gl.bindRenderbuffer(gl.RENDERBUFFER, null) gl.bindFramebuffer(gl.FRAMEBUFFER, null); this.#deleteFBO(this.sharedFBO); // set canvas to smallest size possible gl.canvas.width = 1; gl.canvas.height = 1; // attempt to force a context loss gl.getExtension('WEBGL_lose_context')?.loseContext(); } /** CONSTRUCTION */ /** Create a default quad for cases where a quad is needed (avoid allocation for every quad) */ _createDefaultQuad() { const { gl } = this; // create a vertex array object this.vao = this.buildVAO(); // Create a vertex buffer const ab = new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]); this.vertexBuffer = this.bindEnableVertexAttr(ab, 0, 2, gl.FLOAT, false, 0, 0); // clear vao gl.bindVertexArray(null); } /** * Get the mask for a tile * the zoom determines the number of divisions necessary to maintain a visually * asthetic spherical shape. As we zoom in, the tiles are practically flat, * so division is less useful. * 0, 1 => 16 ; 2, 3 => 8 ; 4, 5 => 4 ; 6, 7 => 2 ; 8+ => 1 * context stores masks so we don't keep recreating them and put excess stress and memory on the GPU * @param division - number of division to slice the geometry by * @param tile - the tile to create the mask for * @returns the mask */ getMask(division, tile) { const { masks } = this; // check if we have a mask for this level let mask = masks.get(division); if (mask === undefined) { mask = buildMask(division, this); masks.set(division, mask); } // we want to mimic the functionality of other draw structures const tileMaskSource = { ...mask, tile, /** internal draw command */ draw: () => { const { fill } = this.painter.workflows; if (fill === undefined) return; // let the context know the current workflow this.setWorkflow(fill); this.stencilFuncAlways(tile.tmpMaskID); // ensure the tile information is set fill.setTileUniforms(tile); fill.drawMask(tileMaskSource); }, /** internal destroy command */ destroy: () => { }, }; return tileMaskSource; } /** Draw a quad */ drawQuad() { const { gl, vao } = this; // bind the vao gl.bindVertexArray(vao); // draw a fan gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); } /* PREP PHASE */ /** Reset the viewport */ resetViewport() { const { gl } = this; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); } /** Bind to the main buffer */ bindMainBuffer() { const { gl } = this; gl.bindFramebuffer(gl.FRAMEBUFFER, null); } /** * Set a clear color for the initialization draws (like the background color) * @param clearColor - the clear color */ setClearColor(clearColor) { this.clearColorRGBA = clearColor; } /** Setup a new scene for future draw calls */ newScene() { const { gl } = this; // ensure we are attached to the main buffer this.bindMainBuffer(); // prep context variables this.clearColor(); this.stencilRef = -1; gl.clearStencil(0x0); gl.clearDepth(1); gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT); } /** Reset the current workflow */ resetWorkflow() { this.currWorkflow = undefined; } /** * Set the current workflow * @param workflow - the workflow to set as the current * @param use - flag to say we want to also activate the workflow */ setWorkflow(workflow, use = true) { if (this.currWorkflow?.label === workflow.label) return; if (use) workflow?.use(); this.currWorkflow = workflow; } /** Clear the interact buffer */ clearInteractBuffer() { const { gl } = this; gl.bindFramebuffer(gl.FRAMEBUFFER, this.interactFramebuffer); gl.clearColor(0, 0, 0, 0); gl.clearStencil(0x0); gl.clear(gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT); } /** Clear the canvas using the current clear color */ clearColor() { const { gl } = this; gl.clearColor(...this.clearColorRGBA); gl.blendColor(0, 0, 0, 0); } /** Clear both the color and depth buffers */ clearColorDepthBuffers() { const { gl } = this; gl.clearColor(0, 0, 0, 0); gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); } /** Clear the color buffer */ clearColorBuffer() { const { gl } = this; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); } /* TEXTURE */ /** * Build a new texture * @param imageData - the raw image data to inject to the texture * @param width - width of the texture * @param height - height of the texture * @param repeat - should the texture repeat * @returns the texture */ buildTexture(imageData, width, height = width, repeat = false) { const { gl } = this; const texture = gl.createTexture(); if (texture === null) throw new Error('Failed to create texture'); gl.bindTexture(gl.TEXTURE_2D, texture); // do not premultiply gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); // set the texture params const param = repeat ? gl.REPEAT : gl.CLAMP_TO_EDGE; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // inject image data. Check if ImageBitmap or ArrayBuffer if (imageData instanceof ImageBitmap) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData); } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData); } return texture; } /** * Update an existing texture * @param texture - the texture to update * @param imageData - the new image data to inject * @param width - the new width * @param height - the new height */ updateTexture(texture, imageData, width, height) { const { gl } = this; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); } /* DEPTH */ /** Enable depth testing */ enableDepthTest() { const { gl, depthState } = this; if (!depthState) { gl.enable(gl.DEPTH_TEST); this.depthState = true; } } /** Disable depth testing */ disableDepthTest() { const { gl, depthState } = this; if (depthState) { gl.disable(gl.DEPTH_TEST); this.depthState = false; } } /** Always pass depth test */ alwaysDepth() { const { gl, zTestMode } = this; if (zTestMode !== 0) { this.zTestMode = 0; gl.depthFunc(gl.ALWAYS); } } /** Depth testing should pass if the depth is less than the reference value */ lessDepth() { const { gl, zTestMode } = this; if (zTestMode !== 1) { this.zTestMode = 1; gl.depthFunc(gl.LESS); } } /** Depth testing should pass if the depth is less than or equal to the reference value */ lequalDepth() { const { gl, zTestMode } = this; if (zTestMode !== 2) { this.zTestMode = 2; gl.depthFunc(gl.LEQUAL); } } /** * Set the depth range * @param depthPos - the depth position */ setDepthRange(depthPos) { const { gl, zLow, zHigh } = this; const depth = 1 - (depthPos + 1) * DEPTH_ESPILON; if (zLow !== depth || zHigh !== depth) { gl.depthRange(depth, depth); this.zLow = this.zHigh = depth; } } /** Reset the depth range to the full depth range */ resetDepthRange() { const { gl, zLow, zHigh } = this; if (zLow !== 0 || zHigh !== 1) { gl.depthRange(0, 1); this.zLow = 0; this.zHigh = 1; } } /* CULLING */ /** Enable face culling */ enableCullFace() { const { gl, cullState } = this; if (!cullState) { gl.enable(gl.CULL_FACE); this.cullState = true; } } /** Disable face culling */ disableCullFace() { const { gl, cullState } = this; if (cullState) { gl.disable(gl.CULL_FACE); this.cullState = false; } } /* BLENDING */ /** Enable blending */ enableBlend() { const { gl, blendState } = this; if (!blendState) { gl.enable(gl.BLEND); this.blendState = true; } } /** Disable blending */ disableBlend() { const { gl, blendState } = this; if (blendState) { gl.disable(gl.BLEND); this.blendState = false; } } /** Set the blending mode to a default state */ defaultBlend() { const { gl, blendMode } = this; this.enableBlend(); if (blendMode !== 0) { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); this.blendMode = 0; } } /** Set the blending mode to blend mode */ shadeBlend() { const { gl, blendMode } = this; this.enableBlend(); if (blendMode !== 1) { gl.blendFunc(gl.DST_COLOR, gl.ZERO); this.blendMode = 1; } } /** Set the blending mode to inversion mode */ inversionBlend() { const { gl, blendMode } = this; this.enableBlend(); if (blendMode !== 2) { gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR); this.blendMode = 2; } } /** Set the blending mode to zero mode */ zeroBlend() { const { gl, blendMode } = this; this.enableBlend(); if (blendMode !== 3) { gl.blendFunc(gl.ZERO, gl.SRC_COLOR); this.blendMode = 3; } } /** Set the blending mode to one mode */ oneBlend() { const { gl, blendMode } = this; this.enableBlend(); if (blendMode !== 4) { gl.blendFunc(gl.ONE, gl.ONE); this.blendMode = 4; } } /* STENCILING */ /** Enable stencil testing */ enableStencilTest() { const { gl, stencilState } = this; if (!stencilState) { gl.enable(gl.STENCIL_TEST); this.stencilState = true; } } /** Disable stencil testing */ disableStencilTest() { const { gl, stencilState } = this; if (stencilState) { gl.disable(gl.STENCIL_TEST); this.stencilState = false; } } /** * Set the stencil function to always pass but you can still update the reference value * @param ref - the reference value */ stencilFuncAlways(ref) { const { gl } = this; if (this.stencilRef === ref) return; this.stencilRef = ref; gl.stencilFunc(gl.ALWAYS, ref, 0xff); } /** * Set the stencil function to pass if the stencil value is equal to the reference value * @param ref - the reference value */ stencilFuncEqual(ref) { const { gl } = this; if (this.stencilRef === ref) return; this.stencilRef = ref; gl.stencilFunc(gl.EQUAL, ref, 0xff); } /** Set the stenci mode to default */ stencilDefault() { const { gl } = this; gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); gl.colorMask(true, true, true, true); } /** Set the stencil mode to invert */ stencilInvert() { const { gl } = this; gl.colorMask(false, false, false, false); gl.stencilOp(gl.KEEP, gl.INVERT, gl.INVERT); gl.stencilFunc(gl.ALWAYS, 0, 0xff); } /** Set the stencil mode to zero */ stencilZero() { const { gl } = this; gl.colorMask(true, true, true, true); gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE); gl.stencilFunc(gl.NOTEQUAL, 0, 0xff); } /* MASKING */ /** enable mask testing */ enableMaskTest() { const { gl } = this; this.defaultBlend(); this.enableCullFace(); this.disableDepthTest(); this.enableStencilTest(); gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); gl.colorMask(false, false, false, false); } /** setup to "flush" a mask's coverage */ flushMask() { this.gl.colorMask(true, true, true, true); } /* VAO */ /** * Build a vertex array object * @returns the vertex array object */ buildVAO() { const { gl } = this; const vao = gl.createVertexArray(); if (vao === null) throw new Error('Failed to create vertex array object'); // and make it the one we're currently working with gl.bindVertexArray(vao); return vao; } /* Attributes */ /** * Bind a vertex attribute * @param ab - the array buffer * @param indx - the index * @param size - the size * @param type - the type * @param normalized - if true, normalize the input data * @param stride - the stride * @param offset - the offset * @param instance - if true, the VAO is used for instancing * @returns the buffer */ bindEnableVertexAttr(ab, indx, size, type, normalized, stride, offset, instance = false) { const buf = this.bindAndBuffer(ab); this.defineBufferState(indx, size, type, normalized, stride, offset, instance); return buf; } /** * Bind mulitiple vertex attribute * @param ab - the array buffer * @param attributes - the collection of attributes that use the same buffer * @param instance - if true, the resulting VAO is used for instancing * @returns the buffer */ bindEnableVertexAttrMulti(ab, // [indx, size, type, normalized, stride, offset] attributes, instance = false) { const buf = this.bindAndBuffer(ab); for (const attr of attributes) this.defineBufferState(...attr, instance); return buf; } /** * Bind and buffer an input array * @param ab - the array buffer * @returns the buffer */ bindAndBuffer(ab) { const { gl } = this; const buf = gl.createBuffer(); if (buf === null) throw Error('Failed to create buffer'); // Bind and buffer gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, ab, gl.STATIC_DRAW); return buf; } /** * Define the state of a vertex attribute * @param indx - the index * @param size - the size * @param type - the type * @param normalized - if true, normalize the input data * @param stride - the stride * @param offset - the offset * @param instance - if true, the VAO is used for instancing */ defineBufferState(indx, size, type, normalized, stride, offset, instance = false) { const { gl } = this; // setup feature attribute gl.enableVertexAttribArray(indx); gl.vertexAttribPointer(indx, size, type, normalized, stride, offset); // instance attribute if needed if (instance) gl.vertexAttribDivisor(indx, 1); } /** * Bind an element array * @param ab - the array buffer to bind * @returns the buffer */ bindElementArray(ab) { const { gl } = this; const buf = gl.createBuffer(); if (buf === null) throw Error('Failed to create buffer'); // Bind and buffer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ab, gl.STATIC_DRAW); return buf; } /* CLEANUP */ /** At the end of rendering a frame/scene, call this function to cleanup the state */ finish() { const { gl } = this; gl.bindVertexArray(null); // gl.bindBuffer(gl.ARRAY_BUFFER, null) // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null) } } /** * A helper function to clean up the renderer string to be more human readable * @param renderer - the renderer string * @returns the cleaned string */ function cleanRenderer(renderer) { return renderer.toLowerCase().replace(/angle \((.+)\)*$/, '$1'); // .replace(/\s+([0-9]+gb|direct3d|opengl.+$)|\(r\)| \([^)]+\)$/g, '') }