UNPKG

s2maps-gpu

Version:

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

432 lines (431 loc) 16.5 kB
import { buildColorRamp } from 'style/color/index.js'; import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import Workflow, { Feature } from './workflow.js'; // WEBGL1 import frag1 from '../shaders/heatmap1.fragment.glsl'; import vert1 from '../shaders/heatmap1.vertex.glsl'; // WEBGL2 import frag2 from '../shaders/heatmap2.fragment.glsl'; import vert2 from '../shaders/heatmap2.vertex.glsl'; /** Heatmap Feature is a standalone heatmap render storage unit that can be drawn to the GPU */ export class HeatmapFeature extends Feature { workflow; source; layerGuide; tile; count; offset; featureCode; parent; bounds; type = 'heatmap'; radiusLo; // webgl1 opacityLo; // webgl1 intensityLo; // webgl1 radiusHi; // webgl1 opacityHi; // webgl1 intensityHi; // webgl1 /** * @param workflow - the heatmap workflow * @param source - the heatmap source * @param layerGuide - layer guide for this feature * @param tile - the tile that the feature is drawn on * @param count - the number of points * @param offset - the offset of the points * @param featureCode - the encoded feature code that tells the GPU how to compute it's properties * @param parent - the parent tile if applicable * @param bounds - the bounds of the tile if applicable */ constructor(workflow, source, layerGuide, tile, count, offset, featureCode = [0], parent, bounds) { super(workflow, tile, layerGuide, featureCode, parent); this.workflow = workflow; this.source = source; this.layerGuide = layerGuide; this.tile = tile; this.count = count; this.offset = offset; this.featureCode = featureCode; this.parent = parent; this.bounds = bounds; } /** * Draw the feature to the GPU * @param interactive - whether or not the feature is interactive */ draw(interactive) { super.draw(interactive); this.workflow.draw(this, interactive); } /** Draw the feature's texture to the GPU */ drawTexture() { const { tile, parent, workflow, layerGuide } = this; const { context } = workflow; const { layerIndex, layerCode, lch } = layerGuide; // ensure the tile information is set workflow.setTileUniforms(tile, parent); // setup stencil context.stencilFuncEqual(tile.tmpMaskID); // set layer code workflow.setLayerCode(layerIndex, layerCode, lch); // tell the workflow to draw the texture this.workflow.drawToTexture(this); } /** * Duplicate this feature * @param tile - the tile that the feature is drawn on * @param parent - the parent tile if applicable * @param bounds - the bounds of the tile if applicable * @returns the duplicated feature */ duplicate(tile, parent, bounds) { const { workflow, source, layerGuide, count, offset, featureCode, radiusLo, opacityLo, intensityLo, radiusHi, opacityHi, intensityHi, } = this; const newFeature = new HeatmapFeature(workflow, source, layerGuide, tile, count, offset, featureCode, parent, bounds); newFeature.setWebGL1Attributes(radiusLo, opacityLo, intensityLo, radiusHi, opacityHi, intensityHi); return newFeature; } /** * Set the webgl1 attributes if the context is webgl1 * Low-High is a system to help WebGL blend between two values on zoom change * @param radiusLo - the low radius * @param opacityLo - the low opacity * @param intensityLo - the low intensity * @param radiusHi - the high radius * @param opacityHi - the high opacity * @param intensityHi - the high intensity */ setWebGL1Attributes(radiusLo, opacityLo, intensityLo, radiusHi, opacityHi, intensityHi) { this.radiusLo = radiusLo; this.opacityLo = opacityLo; this.intensityLo = intensityLo; this.radiusHi = radiusHi; this.opacityHi = opacityHi; this.intensityHi = intensityHi; } } /** Heatmap Workflow */ export default class HeatmapWorkflow extends Workflow { label = 'heatmap'; texture; nullTextureA; nullTextureB; framebuffer; extentBuffer; layerGuides = new Map(); /** @param context - The WebGL(1|2) context */ constructor(context) { // get gl from context const { gl, type, devicePixelRatio } = context; // inject Program super(context); // build shaders if (type === 1) this.buildShaders(vert1, frag1, { aExtent: 0, aPos: 1, aWeight: 2 }); else this.buildShaders(vert2, frag2); // activate so we can setup samplers this.use(); // set device pixel ratio this.setDevicePixelRatio(devicePixelRatio); // set sampler positions const { uColorRamp, uImage } = this.uniforms; gl.uniform1i(uColorRamp, 0); gl.uniform1i(uImage, 1); // build heatmap texture + FBO this.#setupFBO(); } /** Bind the extent/quad buffer */ #bindExtentBuffer() { const { gl, context, extentBuffer } = this; if (extentBuffer === undefined) { // simple quad set // [[-1, -1], [1, -1], [-1, 1]] & [[1, -1], [1, 1], [-1, 1]] const typeArray = new Float32Array([-1, -1, 1, -1, -1, 1, 1, -1, 1, 1, -1, 1]); this.extentBuffer = context.bindEnableVertexAttr(typeArray, 0, 2, gl.FLOAT, false, 0, 0); } else { gl.bindBuffer(gl.ARRAY_BUFFER, extentBuffer); context.defineBufferState(0, 2, gl.FLOAT, false, 0, 0); } } /** * Build the heatmap source * @param heatmapData - the heatmap data sent from the tile worker * @param tile - the tile that the features are drawn on */ buildSource(heatmapData, tile) { const { gl, context } = this; const { featureGuideBuffer } = heatmapData; // prep buffers const vertexA = new Float32Array(heatmapData.vertexBuffer); const weightA = new Float32Array(heatmapData.weightBuffer); // Create a starting vertex array object (attribute state) const vao = context.buildVAO(); // bind buffers to the vertex array object // Create the feature index buffer const vertexBuffer = context.bindEnableVertexAttr(vertexA, 1, 2, gl.FLOAT, false, 8, 0, true); const weightBuffer = context.bindEnableVertexAttr(weightA, 2, 1, gl.FLOAT, false, 4, 0, true); this.#bindExtentBuffer(); const source = { type: 'heatmap', vertexBuffer, weightBuffer, vao, }; context.finish(); // flush vao this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer)); } /** * Build the heatmap features * @param source - the heatmap source * @param tile - the tile that the features are drawn on * @param featureGuideArray - the array of feature guides */ #buildFeatures(source, tile, featureGuideArray) { const features = []; const lgl = featureGuideArray.length; let i = 0; while (i < lgl) { // grab the size, layerIndex, count, and offset, and update the index const [layerIndex, count, offset, encodingSize] = featureGuideArray.slice(i, i + 4); i += 4; // grab the layerGuide const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; // create the feature and set the correct properties const feature = new HeatmapFeature(this, source, layerGuide, tile, count, offset); if (this.type === 1) { const [rLo, oLo, iLo, rHi, oHi, iHi] = featureGuideArray.slice(i, i + 6); feature.setWebGL1Attributes(rLo, oLo, iLo, rHi, oHi, iHi); } else if (this.type === 2 && encodingSize > 0) { feature.featureCode = [...featureGuideArray.slice(i, i + encodingSize)]; } features.push(feature); // update index i += encodingSize; } tile.addFeatures(features); } /** * Build the layer definition * @param layerBase - the common layer attributes * @param layer - the user defined layer attributes * @returns a built layer definition that's ready to describe how to render a feature */ buildLayerDefinition(layerBase, layer) { const { type, context } = this; const { source, layerIndex, lch, visible } = layerBase; // PRE) get layer base // layout let { // paint radius, opacity, intensity, // layout weight, colorRamp, // properties geoFilter, } = layer; radius = radius ?? 1; opacity = opacity ?? 1; intensity = intensity ?? 1; colorRamp = colorRamp ?? 'sinebow'; geoFilter = geoFilter ?? ['line', 'poly']; // 1) build definition const layerDefinition = { ...layerBase, type: 'heatmap', // paint radius, opacity, intensity, // layout weight: weight ?? 1, // properties colorRamp, geoFilter, }; // 2) Store layer workflow, building code if webgl2 const layerCode = []; if (type === 2) { for (const paint of [radius, opacity, intensity]) { layerCode.push(...encodeLayerAttribute(paint, lch)); } } this.layerGuides.set(layerIndex, { sourceName: source, layerIndex, layerCode, lch, colorRamp: context.buildTexture(buildColorRamp(colorRamp, lch), 256, 4), visible, interactive: false, opaque: false, }); return layerDefinition; } /** Setup an FBO for the heatmap. We draw to this FBO before rendering to the main FBO */ #setupFBO() { const { gl, context } = this; this.nullTextureA = context.buildTexture(null, 1); this.nullTextureB = context.buildTexture(null, 1); const texture = (this.texture = context.buildTexture(null, gl.canvas.width, gl.canvas.height)); // create framebuffer const framebuffer = gl.createFramebuffer(); if (framebuffer === null) throw new Error('Failed to create framebuffer'); this.framebuffer = framebuffer; // bind framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // attach texture gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); // we are finished, so go back to our main buffer gl.bindFramebuffer(gl.FRAMEBUFFER, null); } /** Resize the heatmap FBO */ resize() { const { gl, context } = this; context.updateTexture(this.texture, null, gl.canvas.width, gl.canvas.height); } /** Setup the texture draw workflow */ setupTextureDraw() { const { gl, context, uniforms } = this; // set workflow. Will run use, but will also flush. context.setWorkflow(this, false); super.use(); // attach and clear framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); // ensure null textures gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.nullTextureA); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.nullTextureB); // set draw state gl.uniform1f(uniforms.uDrawState, 0); // setup context context.clearColorBuffer(); context.oneBlend(); context.disableCullFace(); context.disableDepthTest(); context.disableStencilTest(); } /** * Draw heatmap feature's textures * @param features - features to draw * @returns the resulting combination of associated features */ textureDraw(features) { if (features.length === 0) return undefined; // setup texture draws this.setupTextureDraw(); // store a feature per layerIndex const output = []; // group by layerIndex const layerFeatures = new Map(); for (const feature of features) { const { layerIndex } = feature.layerGuide; const layer = layerFeatures.get(layerIndex); if (layer === undefined) { layerFeatures.set(layerIndex, [feature]); output.push(feature); } else layer.push(feature); } // draw each layer to their own render target for (const [, features] of layerFeatures.entries()) { for (const feature of features) feature.drawTexture(); } return output; } /** * Draw feature to early FBO * @param featureGuide - feature to draw */ drawToTexture(featureGuide) { // grab context const { context, uniforms } = this; const { gl, type, defaultBounds } = context; // get current source data const { count, offset, source, featureCode, bounds } = featureGuide; const { radiusLo, opacityLo, intensityLo, radiusHi, opacityHi, intensityHi } = featureGuide; const { uRadiusLo, uOpacityLo, uIntensityLo, uRadiusHi, uOpacityHi, uIntensityHi, uBounds } = uniforms; const { vao, vertexBuffer, weightBuffer } = source; // set feature code (webgl 1 we store the colors, webgl 2 we store layerCode lookups) if (type === 1) { gl.uniform1f(uRadiusLo, radiusLo ?? 1); gl.uniform1f(uOpacityLo, opacityLo ?? 1); gl.uniform1f(uIntensityLo, intensityLo ?? 1); gl.uniform1f(uRadiusHi, radiusHi ?? 1); gl.uniform1f(uOpacityHi, opacityHi ?? 1); gl.uniform1f(uIntensityHi, intensityHi ?? 1); } else { this.setFeatureCode(featureCode); } // if bounds exists, set them, otherwise set default bounds if (bounds !== undefined) gl.uniform4fv(uBounds, bounds); else gl.uniform4fv(uBounds, defaultBounds); // setup offsets and draw gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 8, offset * 8); gl.bindBuffer(gl.ARRAY_BUFFER, weightBuffer); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 4, offset * 4); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count); } /** Use the heatmap workflow */ use() { super.use(); const { gl, context, uniforms } = this; // set draw state gl.uniform1f(uniforms.uDrawState, 1); // revert back to texture 0 gl.activeTexture(gl.TEXTURE0); // setup context context.defaultBlend(); context.enableDepthTest(); context.enableStencilTest(); context.disableCullFace(); context.lessDepth(); } /** * Draw the heatmap feature * @param feature - feature to draw * @param _interactive - whether or not the feature is interactive */ draw(feature, _interactive = false) { // grab the context const { gl, context } = this; const { vao } = context; // get current feature data const { layerGuide: { layerIndex, visible, colorRamp }, } = feature; if (!visible) return; // set context's full screen fbo gl.bindVertexArray(vao); // set colorRamp gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, colorRamp); // adjust context context.stencilFuncAlways(0); context.setDepthRange(layerIndex); // draw a fan gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); } /** Delete the heatmap workflow */ delete() { const { gl, texture, framebuffer } = this; // delete texture gl.deleteTexture(texture); // delete framebuffer gl.deleteFramebuffer(framebuffer); // cleanup super.delete(); } }