UNPKG

s2maps-gpu

Version:

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

327 lines (326 loc) 12.7 kB
import { buildDashImage } from 'style/color/index.js'; import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import Workflow, { Feature } from './workflow.js'; // WEBGL1 import frag1 from '../shaders/line1.fragment.glsl'; import vert1 from '../shaders/line1.vertex.glsl'; // WEBGL2 import frag2 from '../shaders/line2.fragment.glsl'; import vert2 from '../shaders/line2.vertex.glsl'; /** Line Feature is a standalone line render storage unit that can be drawn to the GPU */ export class LineFeature extends Feature { workflow; layerGuide; source; tile; count; offset; featureCode; cap; parent; type = 'line'; color; // webgl1 opacity; // webgl1 width; // webgl1 gapwidth; // webgl1 /** * @param workflow - the line workflow * @param layerGuide - layer guide for this feature * @param source - the line source * @param tile - the tile that the feature is drawn on * @param count - the number of lines * @param offset - the offset of the lines * @param featureCode - the encoded feature that tells the GPU how to compute it's properties * @param cap - the line cap * @param parent - the parent tile */ constructor(workflow, layerGuide, source, tile, count, offset, featureCode, cap, parent) { super(workflow, tile, layerGuide, featureCode, parent); this.workflow = workflow; this.layerGuide = layerGuide; this.source = source; this.tile = tile; this.count = count; this.offset = offset; this.featureCode = featureCode; this.cap = cap; this.parent = parent; } /** * 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 * @returns the duplicated feature */ duplicate(tile, parent) { const { workflow, layerGuide, source, count, offset, featureCode, cap, color, opacity, width, gapwidth, } = this; const newFeature = new LineFeature(workflow, layerGuide, source, tile, count, offset, featureCode, cap, parent); newFeature.setWebGL1Attributes(color, opacity, width, gapwidth); return newFeature; } /** * Set the attributes of the feature if the context is webgl1 * @param color - the color * @param opacity - the opacity * @param width - the width * @param gapwidth - the gapwidth */ setWebGL1Attributes(color, opacity, width, gapwidth) { this.color = color; this.opacity = opacity; this.width = width; this.gapwidth = gapwidth; } } /** Line Workflow */ export default class LineWorkflow extends Workflow { label = 'line'; curTexture = -1; typeBuffer; 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, { aType: 0, aPrev: 1, aCurr: 2, aNext: 3, aLengthSoFar: 4 }); else this.buildShaders(vert2, frag2); // activate so we can setup samplers this.use(); // create the null texture align with line gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, context.nullTexture); // set device pixel ratio this.setDevicePixelRatio(devicePixelRatio); } /** Bind the quad type buffer. Describes how to draw a quad based upon the prev->curr->next */ #bindTypeBuffer() { const { gl, context, typeBuffer } = this; if (typeBuffer === undefined) { // 0 -> curr // 1 -> curr + (-1 * normal) // 2 -> curr + (normal) // 3 -> next + (-1 * normal) // 4 -> next + (normal) // 5 -> curr + (normal) [check that prev, curr, and next is CCW otherwise invert normal] // 6 -> curr + (previous-normal) [check that prev, curr, and next is CCW otherwise invert normal] const typeArray = new Float32Array([1, 3, 4, 1, 4, 2, 0, 5, 6]); this.typeBuffer = context.bindEnableVertexAttr(typeArray, 0, 1, gl.FLOAT, false, 0, 0); } else { gl.bindBuffer(gl.ARRAY_BUFFER, typeBuffer); context.defineBufferState(0, 1, gl.FLOAT, false, 0, 0); } } /** * 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 { context } = this; const { type } = this; const { source, layerIndex, lch, visible } = layerBase; // PRE) get layer base let { interactive, cursor, geoFilter, // paint color, opacity, width, gapwidth, // layout cap, join, dasharray, } = layer; color = color ?? 'rgba(0, 0, 0, 0)'; opacity = opacity ?? 1; width = width ?? 1; gapwidth = gapwidth ?? 0; geoFilter = geoFilter ?? []; // 1) build definition const dashed = Array.isArray(dasharray) && dasharray.length > 0; interactive = interactive ?? false; cursor = cursor ?? 'default'; dasharray = dasharray ?? []; const layerDefinition = { ...layerBase, type: 'line', color, opacity, width, gapwidth, cap: cap ?? 'butt', join: join ?? 'miter', dasharray, geoFilter, dashed, interactive, cursor, }; // 2) Store layer workflow, building code if webgl2 const layerCode = []; if (type === 2) { for (const paint of [color, opacity, width, gapwidth]) { layerCode.push(...encodeLayerAttribute(paint, lch)); } } // if dashed, build a texture const { length, dashCount, image } = buildDashImage(dasharray, context.devicePixelRatio); const dashTexture = length > 0 ? context.buildTexture(image, length, 5) : context.nullTexture; this.layerGuides.set(layerIndex, { sourceName: source, layerIndex, layerCode, lch, dashed, dashCount, dashLength: length, dashTexture, interactive, cursor, visible, opaque: false, }); return layerDefinition; } /** * Build the line source * @param lineData - raw input data from the Tile Worker * @param tile - the tile that the feature is drawn on */ buildSource(lineData, tile) { const { gl, context } = this; const { featureGuideBuffer } = lineData; // prep buffers const vertexA = new Int16Array(lineData.vertexBuffer); const lengthSoFarA = new Float32Array(lineData.lengthSoFarBuffer); // const fillIDA = new Uint8Array(lineData.fillIDBuffer) // Create a starting vertex array object (attribute state) 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); // bind buffers to the vertex array object const vertexBuffer = context.bindEnableVertexAttrMulti(vertexA, [ // [indx, size, type, normalized, stride, offset] [1, 2, gl.FLOAT, false, 24, 0], [2, 2, gl.FLOAT, false, 24, 8], [3, 2, gl.FLOAT, false, 24, 16], ], true); const lengthSoFarBuffer = context.bindEnableVertexAttr(lengthSoFarA, 4, 1, gl.FLOAT, false, 4, 0, true); // const fillIDBuffer = context.bindEnableVertexAttr(fillIDA, 6, 3, gl.UNSIGNED_BYTE, true, 0, 0) // bind the typeBuffer this.#bindTypeBuffer(); const source = { type: 'line', vertexBuffer, lengthSoFarBuffer, // fillIDBuffer, vao, }; context.finish(); // flush vao this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer)); } /** * Build the line features * @param source - the line source code * @param tile - the tile that the feature is drawn on * @param featureGuideArray - the feature guide */ #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 [cap, layerIndex, count, offset, encodingSize] = featureGuideArray.slice(i, i + 5); i += 5; // pull in the layerGuide const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; // build feature const feature = new LineFeature(this, layerGuide, source, tile, count, offset, [0], cap); if (this.type === 1) { const [r, g, b, a, o, w, gw] = featureGuideArray.slice(i, i + 7); feature.setWebGL1Attributes([r, g, b, a], o, w, gw); i += 7; } else if (this.type === 2 && encodingSize > 0) { feature.featureCode = [...featureGuideArray.slice(i, i + encodingSize)]; // update index i += encodingSize; } features.push(feature); } tile.addFeatures(features); } /** Use this workflow as the current shaders for the GPU */ use() { super.use(); const { context } = this; const { gl } = context; // setup context context.defaultBlend(); context.disableCullFace(); context.enableDepthTest(); context.enableStencilTest(); context.lequalDepth(); gl.activeTexture(gl.TEXTURE0); } /** * Draw a line feature * @param feature - the line feature * @param _interactive - whether or not the feature is interactive */ draw(feature, _interactive = false) { // grab context const { gl, context, type, uniforms } = this; const { uCap, uDashed, uDashCount, uTexLength, uColor, uOpacity, uWidth } = uniforms; // get current source data const { count, offset, featureCode, source, cap, color, opacity, width, layerGuide: { dashed, dashCount, dashLength, dashTexture, layerIndex, visible }, } = feature; if (!visible) return; const { vao, vertexBuffer, lengthSoFarBuffer } = source; context.setDepthRange(layerIndex); // set cap and dashed gl.uniform1f(uCap, cap); gl.uniform1i(uDashed, ~~dashed); // ensure a dash texture is mapped, if feature isn't dashed, use nullTexture if (dashed) { this.curTexture = layerIndex; gl.uniform1f(uTexLength, dashLength); gl.uniform1f(uDashCount, dashCount); gl.bindTexture(gl.TEXTURE_2D, dashTexture); } // set feature code if (type === 1) { gl.uniform4fv(uColor, color ?? [0, 0, 0, 1]); gl.uniform1f(uOpacity, opacity ?? 1); gl.uniform1f(uWidth, width ?? 1); } else { this.setFeatureCode(featureCode); } // bind vao gl.bindVertexArray(vao); // apply the appropriate offset in the source vertexBuffer attribute gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 24, 0 + offset * 24); gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 8 + offset * 24); gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 24, 16 + offset * 24); gl.bindBuffer(gl.ARRAY_BUFFER, lengthSoFarBuffer); gl.vertexAttribPointer(4, 1, gl.FLOAT, false, 0, offset * 4); // draw elements gl.drawArraysInstanced(gl.TRIANGLES, 0, 9, count); // gl.drawArraysInstancedANGLE(mode, first, count, primcount) } }