UNPKG

s2maps-gpu

Version:

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

265 lines (264 loc) 9.93 kB
import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import Workflow, { Feature } from './workflow.js'; // WEBGL1 import frag1 from '../shaders/hillshade1.fragment.glsl'; import vert1 from '../shaders/hillshade1.vertex.glsl'; // WEBGL2 import frag2 from '../shaders/hillshade2.fragment.glsl'; import vert2 from '../shaders/hillshade2.vertex.glsl'; /** Hillshade Feature is a standalone hillshade render storage unit that can be drawn to the GPU */ export class HilllshadeFeature extends Feature { workflow; layerGuide; tile; source; fadeStartTime; parent; type = 'hillshade'; opacity; // webgl1 shadowColor; // webgl1 accentColor; // webgl1 highlightColor; // webgl1 azimuth; // webgl1 altitude; // webgl1 /** * @param workflow - the hillshade workflow * @param layerGuide - layer guide for this feature * @param tile - the tile that the feature is drawn on * @param source - the raster source * @param fadeStartTime - the start time of the fade * @param parent - the parent tile */ constructor(workflow, layerGuide, tile, source, fadeStartTime = Date.now(), parent) { super(workflow, tile, layerGuide, [0], parent); this.workflow = workflow; this.layerGuide = layerGuide; this.tile = tile; this.source = source; this.fadeStartTime = fadeStartTime; 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 { layerGuide, workflow, source, fadeStartTime, opacity, shadowColor, accentColor, highlightColor, azimuth, altitude, } = this; const newFeature = new HilllshadeFeature(workflow, layerGuide, tile, source, fadeStartTime, parent); newFeature.setWebGL1Attributes(opacity, shadowColor, accentColor, highlightColor, azimuth, altitude); return newFeature; } /** * Set the attributes of the feature if the context is webgl1 * @param opacity - the opacity * @param shadowColor - the shadow color * @param accentColor - the accent color * @param highlightColor - the highlight color * @param azimuth - the azimuth * @param altitude - the altitude */ setWebGL1Attributes(opacity, shadowColor, accentColor, highlightColor, azimuth, altitude) { this.opacity = opacity; this.shadowColor = shadowColor; this.accentColor = accentColor; this.highlightColor = highlightColor; this.azimuth = azimuth; this.altitude = altitude; } /** * Set the attributes of the feature if the context is webgl1 * @param code - the code */ setWebGL1AttributesCode(code) { this.setWebGL1Attributes(code[0], code.slice(1, 5), code.slice(5, 9), code.slice(9, 13), code[13], code[14]); } } /** Hillshade Workflow */ export default class HillshadeWorkflow extends Workflow { label = 'hillshade'; layerGuides = new Map(); /** @param context - the WebGL(1|2) context */ constructor(context) { // get gl from context const { gl, type } = context; // inject Program super(context); // build shaders if (type === 1) this.buildShaders(vert1, frag1, { aPos: 0 }); else this.buildShaders(vert2, frag2); // activate so we can setup samplers this.use(); // set sampler positions const { uTexture } = this.uniforms; gl.uniform1i(uTexture, 0); } /** * Build the hillshade source * @param hillshadeData - the hillshade data sent from the Tile Worker * @param tile - the tile that the feature is drawn on */ buildSource(hillshadeData, tile) { const { gl, context } = this; const { image, size } = hillshadeData; // do not premultiply gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); // setup texture params const texture = context.buildTexture(image, size); // create the soruce const source = { type: 'raster', texture, size }; // build features this.#buildFeatures(source, hillshadeData, tile); } /** * Build the hillshade features * @param source - the source * @param hillshadeData - the hillshade data * @param tile - the tile that the features are drawn on */ #buildFeatures(source, hillshadeData, tile) { const { featureGuides } = hillshadeData; // for each layer that maches the source, build the feature const features = []; for (const { code, layerIndex } of featureGuides) { const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; const feature = new HilllshadeFeature(this, layerGuide, tile, source); if (this.type === 1) feature.setWebGL1AttributesCode(code); features.push(feature); } 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 } = this; const { source, layerIndex, lch, visible, interactive } = layerBase; // PRE) get layer properties let { unpack, shadowColor, accentColor, highlightColor, opacity, azimuth, altitude, fadeDuration, } = layer; shadowColor = shadowColor ?? '#000'; accentColor = accentColor ?? '#000'; highlightColor = highlightColor ?? '#fff'; opacity = opacity ?? 1; azimuth = azimuth ?? 315; altitude = altitude ?? 45; fadeDuration = fadeDuration ?? 300; // defaults to mapbox unpack unpack = unpack ?? { offset: -10000, zFactor: 0.1, aMultiplier: 0, bMultiplier: 1, gMultiplier: 256, rMultiplier: 256 * 256, }; // 1) build definition const layerDefinition = { ...layerBase, type: 'hillshade', shadowColor, accentColor, highlightColor, azimuth, altitude, opacity, unpack, }; // 2) Store layer workflow, building code if webgl2 const layerCode = []; if (type === 2) { for (const paint of [opacity, shadowColor, accentColor, highlightColor, azimuth, altitude]) { layerCode.push(...encodeLayerAttribute(paint, lch)); } } // 3) Store layer guide const unpackData = [ unpack.offset, unpack.zFactor, unpack.rMultiplier, unpack.gMultiplier, unpack.bMultiplier, unpack.aMultiplier, ]; this.layerGuides.set(layerIndex, { sourceName: source, layerIndex, layerCode, lch, fadeDuration, unpack: unpackData, visible, interactive: interactive ?? false, opaque: false, }); return layerDefinition; } /** Use this workflow as the current shaders for the GPU */ use() { super.use(); const { context } = this; // setup context context.defaultBlend(); context.enableDepthTest(); context.enableCullFace(); context.enableStencilTest(); context.lessDepth(); } /** * Draw the hillshade feature * @param feature - the feature guide * @param _interactive - whether or not the feature is interactive */ draw(feature, _interactive = false) { // grab gl from the context const { type, gl, context, uniforms } = this; const { uFade, uTexLength, uUnpack, uOpacity, uShadowColor, uAccentColor, uHighlightColor, uAzimuth, uAltitude, } = uniforms; const { PI, min, max } = Math; // get current source data const { tile, parent, source, layerGuide: { layerIndex, visible, unpack }, featureCode, opacity, shadowColor, accentColor, highlightColor, azimuth, altitude, } = feature; if (!visible) return; const { texture, size } = source; const { vao, count, offset } = (parent ?? tile).mask; context.setDepthRange(layerIndex); // set fade gl.uniform1f(uFade, 1); gl.uniform1fv(uUnpack, unpack); // set feature code (webgl 1 we store the opacity, webgl 2 we store layerCode lookups) if (type === 1) { gl.uniform1f(uTexLength, size); gl.uniform1f(uOpacity, opacity ?? 1); gl.uniform4fv(uShadowColor, shadowColor ?? [0, 0, 0, 1]); gl.uniform4fv(uAccentColor, accentColor ?? [0, 0, 0, 1]); gl.uniform4fv(uHighlightColor, highlightColor ?? [1, 1, 1, 1]); gl.uniform1f(uAzimuth, (min(max(azimuth ?? 315, 0), 360) * PI) / 180); gl.uniform1f(uAltitude, min(max(altitude ?? 45, 0), 90) / 90); } else { this.setFeatureCode(featureCode); } // bind vao gl.bindVertexArray(vao); // setup the texture gl.bindTexture(gl.TEXTURE_2D, texture); // draw elements gl.drawElements(gl.TRIANGLE_STRIP, count, gl.UNSIGNED_INT, offset); } }