UNPKG

s2maps-gpu

Version:

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

479 lines (478 loc) 19.5 kB
import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import Workflow, { Feature } from './workflow.js'; // WEBGL1 import frag1 from '../shaders/glyph1.fragment.glsl'; import vert1 from '../shaders/glyph1.vertex.glsl'; // WEBGL2 import frag2 from '../shaders/glyph2.fragment.glsl'; import vert2 from '../shaders/glyph2.vertex.glsl'; /** Glyph Feature is a standalone glyph render storage unit that can be drawn to the GPU */ export class GlyphFeature extends Feature { workflow; source; tile; layerGuide; count; offset; filterCount; filterOffset; isPath; isIcon; featureCode; parent; bounds; type = 'glyph'; size; // webgl1 fill; // webgl1 stroke; // webgl1 strokeWidth; // webgl1 /** * @param workflow - the glyph workflow * @param source - the glyph source * @param tile - the tile that the feature is drawn on * @param layerGuide - layer guide for this feature * @param count - the number of glyphs * @param offset - the offset of the glyphs * @param filterCount - the number of filter glyphs * @param filterOffset - the offset of the filter glyphs * @param isPath - whether or not the glyph is a path or a point * @param isIcon - whether or not the glyph is an icon or a standard glyph * @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, tile, layerGuide, count, offset, filterCount, filterOffset, isPath, isIcon, featureCode, parent, bounds) { super(workflow, tile, layerGuide, featureCode, parent, bounds); this.workflow = workflow; this.source = source; this.tile = tile; this.layerGuide = layerGuide; this.count = count; this.offset = offset; this.filterCount = filterCount; this.filterOffset = filterOffset; this.isPath = isPath; this.isIcon = isIcon; 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 = false) { super.draw(interactive); this.workflow.draw(this, interactive); } /** * 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, filterCount, filterOffset, isPath, isIcon, featureCode, size, fill, stroke, strokeWidth, } = this; const newFeature = new GlyphFeature(workflow, source, tile, layerGuide, count, offset, filterCount, filterOffset, isPath, isIcon, featureCode, parent, bounds); this.setWebGL1Attributes(size, fill, stroke, strokeWidth); return newFeature; } /** * Set the attributes of the feature if the context is webgl1 * @param size - size * @param fill - fill * @param stroke - stroke * @param strokeWidth - stroke width */ setWebGL1Attributes(size, fill, stroke, strokeWidth) { this.size = size; this.fill = fill; this.stroke = stroke; this.strokeWidth = strokeWidth; } } /** Glyph Workflow */ export default class GlyphWorkflow extends Workflow { label = 'glyph'; stepBuffer; uvBuffer; glyphFilterWorkflow; 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 const attributeLocations = { aUV: 0, aST: 1, aXY: 2, aOffset: 3, aWH: 4, aTexXY: 5, aTexWH: 6, aID: 7, aColor: 8, }; if (type === 1) this.buildShaders(vert1, frag1, attributeLocations); else this.buildShaders(vert2, frag2); // activate so we can setup samplers this.use(); const { uFeatures, uGlyphTex, uTexSize } = this.uniforms; // set texture positions gl.uniform1i(uFeatures, 0); // uFeatures texture unit 0 gl.uniform1i(uGlyphTex, 1); // uGlyphTex texture unit 1 // setup the devicePixelRatio this.setDevicePixelRatio(devicePixelRatio); // set the current fbo size gl.uniform2fv(uTexSize, context.sharedFBO.texSize); } /** Bind the step uniform buffer */ #bindStepBuffer() { const { gl, context, stepBuffer } = this; if (stepBuffer === undefined) { const stepVerts = new Float32Array([0, 1]); this.stepBuffer = context.bindEnableVertexAttr(stepVerts, 0, 1, gl.FLOAT, false, 0, 0); } else { gl.bindBuffer(gl.ARRAY_BUFFER, stepBuffer); context.defineBufferState(0, 1, gl.FLOAT, false, 0, 0); } } /** Bind the uv buffer */ #bindUVBuffer() { const { gl, context, uvBuffer } = this; if (uvBuffer === undefined) { const uvVerts = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]); this.uvBuffer = context.bindEnableVertexAttr(uvVerts, 0, 2, gl.FLOAT, false, 0, 0); } else { gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer); context.defineBufferState(0, 2, gl.FLOAT, false, 0, 0); } } /** * Inject the glyph filter workflow to share the glyph filter texture * @param glyphFilterWorkflow - The glyph filter workflow */ injectFilter(glyphFilterWorkflow) { this.glyphFilterWorkflow = glyphFilterWorkflow; } /** * Build features from the glyph source sent from the Tile Worker * @param glyphData - The glyph data from the Tile Worker * @param tile - The tile that the feature is drawn on */ buildSource(glyphData, tile) { const { gl, context } = this; const { featureGuideBuffer } = glyphData; // STEP 1 - FILTER const filterVAO = context.buildVAO(); // Create the UV buffer this.#bindStepBuffer(); // create the boxVertex buffer const glyphFilterVerts = new Float32Array(glyphData.glyphFilterBuffer); const glyphFilterBuffer = context.bindEnableVertexAttrMulti(glyphFilterVerts, [ // [indx, size, type, normalized, stride, offset] [1, 2, gl.FLOAT, false, 44, 0], // st [2, 2, gl.FLOAT, false, 44, 8], // xy [3, 2, gl.FLOAT, false, 44, 16], // offset [4, 2, gl.FLOAT, false, 44, 24], // padding [5, 2, gl.FLOAT, false, 44, 32], // wh [6, 1, gl.FLOAT, false, 44, 40], // index ], true); // id buffer const glyphFilterIDs = new Uint8Array(glyphData.glyphFilterIDBuffer); const glyphFilterIDBuffer = context.bindEnableVertexAttr(glyphFilterIDs, 7, 4, gl.UNSIGNED_BYTE, true, 4, 0, true); // STEP 2 - QUADS const vao = context.buildVAO(); // Create the UV buffer this.#bindUVBuffer(); // create the vertex and color buffers const glyphQuadVerts = new Float32Array(glyphData.glyphQuadBuffer); const glyphQuadBuffer = context.bindEnableVertexAttrMulti(glyphQuadVerts, [ // [indx, size, type, normalized, stride, offset] [1, 2, gl.FLOAT, false, 48, 0], // st [2, 2, gl.FLOAT, false, 48, 8], // xy [3, 2, gl.FLOAT, false, 48, 16], // offsetXY [4, 2, gl.FLOAT, false, 48, 24], // wh [5, 2, gl.FLOAT, false, 48, 32], // textureXY [6, 2, gl.FLOAT, false, 48, 40], // texture-width, texture-height ], true); // create id buffer const glyphQuadIDs = new Uint8Array(glyphData.glyphQuadIDBuffer); const glyphQuadIDBuffer = context.bindEnableVertexAttr(glyphQuadIDs, 7, 4, gl.UNSIGNED_BYTE, true, 4, 0, true); // create the vertex and color buffers const glyphColorVerts = new Uint8Array(glyphData.glyphColorBuffer); const glyphColorBuffer = context.bindEnableVertexAttr(glyphColorVerts, 8, 4, gl.UNSIGNED_BYTE, true, 4, 0, true); const source = { type: 'glyph', glyphFilterBuffer, glyphFilterIDBuffer, glyphQuadBuffer, glyphQuadIDBuffer, glyphColorBuffer, filterVAO, vao, }; // cleanup context.finish(); this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer)); } /** * Build the features * @param source - the glyph source * @param tile - the tile that the feature is 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) { // curlayerIndex, curType, filterOffset, filterCount, quadOffset, quadCount, encoding.length, ...encoding const [layerIndex, isPath, isIcon, filterOffset, filterCount, offset, count, encodingSize] = featureGuideArray.slice(i, i + 8); i += 8; // grab the layerGuide const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; // create the feature const feature = new GlyphFeature(this, source, tile, layerGuide, count, offset, filterCount, filterOffset, isPath === 1, isIcon === 1, [0]); if (this.type === 1) { if (isIcon === 0) { // text feature.setWebGL1Attributes(featureGuideArray[i], [...featureGuideArray.slice(i + 1, i + 5)], [...featureGuideArray.slice(i + 6, i + 10)], featureGuideArray[i + 5]); } else { // icon feature.size = featureGuideArray[i]; } } 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 for this workflow given the user input layer * @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 } = this; const { source, layerIndex, lch, visible } = layerBase; // PRE) get layer base // layout const { placement, spacing, textFamily, textField, textAnchor } = layer; const { textOffset, textPadding, textWordWrap, textAlign, textKerning, textLineHeight } = layer; const { iconFamily, iconField, iconAnchor, iconOffset, iconPadding } = layer; // paint let { textSize, iconSize, textFill, textStrokeWidth, textStroke } = layer; // properties let { interactive, cursor, overdraw, viewCollisions, noShaping, geoFilter } = layer; textSize = textSize ?? 16; iconSize = iconSize ?? 16; textFill = textFill ?? 'rgb(0, 0, 0)'; textStrokeWidth = textStrokeWidth ?? 0; textStroke = textStroke ?? 'rgb(0, 0, 0)'; interactive = interactive ?? false; cursor = cursor ?? 'default'; overdraw = overdraw ?? false; viewCollisions = viewCollisions ?? false; noShaping = noShaping ?? false; geoFilter = geoFilter ?? []; // 1) build definition const layerDefinition = { ...layerBase, type: 'glyph', // paint textSize, iconSize, textFill, textStrokeWidth, textStroke, // layout placement: placement ?? 'line', spacing: spacing ?? 325, textFamily: textFamily ?? '', textField: textField ?? '', textAnchor: textAnchor ?? 'center', textOffset: textOffset ?? [0, 0], textPadding: textPadding ?? [0, 0], textWordWrap: textWordWrap ?? 0, textAlign: textAlign ?? 'center', textKerning: textKerning ?? 0, textLineHeight: textLineHeight ?? 0, iconFamily: iconFamily ?? '', iconField: iconField ?? '', iconAnchor: iconAnchor ?? 'center', iconOffset: iconOffset ?? [0, 0], iconPadding: iconPadding ?? [0, 0], // properties viewCollisions, noShaping, interactive, cursor, overdraw, geoFilter, }; // 2) Store layer workflow, building code if webgl2 const layerCode = []; if (type === 2) { for (const value of [textSize, iconSize, textFill, textStrokeWidth, textStroke]) { layerCode.push(...encodeLayerAttribute(value, lch)); } } this.layerGuides.set(layerIndex, { sourceName: source, layerIndex, layerCode, lch, interactive, cursor, overdraw, viewCollisions, visible, opaque: false, }); return layerDefinition; } /** * Compute the glyph filters so that we know which glyphs to render * @param glyphFeatures - the glyph features that need to be computed */ computeFilters(glyphFeatures) { const { glyphFilterWorkflow } = this; glyphFilterWorkflow.use(); // Step 1: draw quads glyphFilterWorkflow.bindQuadFrameBuffer(); this.#computeFilters(glyphFeatures, 1); // Step 2: draw result points glyphFilterWorkflow.bindResultFramebuffer(); this.#computeFilters(glyphFeatures, 2); } /** * Compute the glyph filters via step functions, First step is to render positions, second step is to describe what's been filtered * @param glyphFeatures - the glyph features * @param mode - the draw mode (1 or 2) */ #computeFilters(glyphFeatures, mode) { const { context, glyphFilterWorkflow } = this; const { gl } = context; let curLayer = -1; // set mode glyphFilterWorkflow.setMode(mode); // draw each feature for (const glyphFeature of glyphFeatures) { const { tile, parent, layerGuide: { layerIndex, layerCode, lch }, source, } = glyphFeature; // update layerIndex if (curLayer !== layerIndex) { curLayer = layerIndex; glyphFilterWorkflow.setLayerCode(layerIndex, layerCode, lch); } glyphFilterWorkflow.setTileUniforms(parent ?? tile); gl.bindVertexArray(source.filterVAO); // draw glyphFilterWorkflow.draw(glyphFeature, false); } } /** Use this workflow as the current shaders for the GPU */ use() { super.use(); const { context, uniforms } = this; const { gl, sharedFBO } = context; // prepare context context.defaultBlend(); context.enableDepthTest(); context.disableCullFace(); context.disableStencilTest(); // set the texture size uniform gl.uniform2fv(uniforms.uTexSize, sharedFBO.texSize); } /** * Draw the glyph feature * @param feature - the glyph feature guide * @param interactive - whether or not the feature is interactive */ draw(feature, interactive = false) { const { gl, context, glyphFilterWorkflow, uniforms } = this; const { type, defaultBounds, sharedFBO } = context; const { uSize, uFill, uStroke, uSWidth, uBounds, uIsStroke } = uniforms; // pull out the appropriate data from the source const { source, isIcon, layerGuide: { layerIndex, visible, overdraw }, featureCode, offset, count, size, fill, stroke, strokeWidth, bounds, } = feature; if (!visible) return; const { glyphQuadBuffer, glyphQuadIDBuffer, glyphColorBuffer, vao } = source; // grab glyph texture const { texture } = sharedFBO; // WebGL1 - set paint properties; WebGL2 - set feature code if (type === 1) { gl.uniform1f(uSize, size ?? 0); gl.uniform4fv(uFill, fill ?? [0, 0, 0, 1]); gl.uniform4fv(uStroke, stroke ?? [0, 0, 0, 1]); gl.uniform1f(uSWidth, strokeWidth ?? 0); } 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); // set depth type if (interactive) context.lessDepth(); else context.lequalDepth(); // context.lequalDepth() context.setDepthRange(layerIndex); // set overdraw gl.uniform1i(uniforms.uOverdraw, ~~overdraw); // set draw type gl.uniform1i(uniforms.uIsIcon, ~~isIcon); // bind the correct glyph texture gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, texture); // ensure glyphFilterWorkflow's result texture is set gl.activeTexture(gl.TEXTURE0); glyphFilterWorkflow.bindResultTexture(); // use vao gl.bindVertexArray(vao); // apply the appropriate offset in the source vertexBuffer attribute gl.bindBuffer(gl.ARRAY_BUFFER, glyphQuadBuffer); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 48, offset * 48); // s, t gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 48, 8 + offset * 48); // x, y gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 48, 16 + offset * 48); // xOffset, yOffset gl.vertexAttribPointer(4, 2, gl.FLOAT, false, 48, 24 + offset * 48); // width, height gl.vertexAttribPointer(5, 2, gl.FLOAT, false, 48, 32 + offset * 48); // texture x, y gl.vertexAttribPointer(6, 2, gl.FLOAT, false, 48, 40 + offset * 48); // width, height gl.bindBuffer(gl.ARRAY_BUFFER, glyphQuadIDBuffer); gl.vertexAttribPointer(7, 4, gl.UNSIGNED_BYTE, true, 4, offset * 4); gl.bindBuffer(gl.ARRAY_BUFFER, glyphColorBuffer); gl.vertexAttribPointer(8, 4, gl.UNSIGNED_BYTE, true, 4, offset * 4); // draw. If type is "text" than draw the stroke first, then fill if (!isIcon) { gl.uniform1i(uIsStroke, 1); gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, count); gl.uniform1i(uIsStroke, 0); } gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, count); } /** Delete the glyph workflow */ delete() { // continue forward super.delete(); } }