s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
348 lines (347 loc) • 13.5 kB
JavaScript
import { colorFunc } from 'workers/process/vectorWorker.js';
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
import parseFeature from 'style/parseFeature.js';
import Workflow, { Feature } from './workflow.js';
// WEBGL1
import frag1 from '../shaders/fill1.fragment.glsl';
import vert1 from '../shaders/fill1.vertex.glsl';
// WEBGL2
import frag2 from '../shaders/fill2.fragment.glsl';
import vert2 from '../shaders/fill2.vertex.glsl';
/** Fill Feature is a standalone fill render storage unit that can be drawn to the GPU */
export class FillFeature extends Feature {
workflow;
layerGuide;
maskLayer;
source;
mode;
count;
offset;
patternXY;
patternWH;
patternMovement;
featureCode;
tile;
parent;
type = 'fill';
color; // webgl1
opacity; // webgl1
/**
* @param workflow - the fill workflow
* @param layerGuide - layer guide for this feature
* @param maskLayer - whether or not the layer is a mask or a fill
* @param source - the fill or mask source
* @param mode - the draw mode
* @param count - the number of points
* @param offset - the offset of the points
* @param patternXY - the pattern offset
* @param patternWH - the pattern size
* @param patternMovement - the pattern movement position
* @param featureCode - the feature code that tells the GPU how to compute it's properties
* @param tile - the tile that the feature is drawn on
* @param parent - the parent tile if applicable
*/
constructor(workflow, layerGuide, maskLayer, source, mode, count, offset, patternXY, patternWH, patternMovement, featureCode, tile, parent) {
super(workflow, tile, layerGuide, featureCode, parent);
this.workflow = workflow;
this.layerGuide = layerGuide;
this.maskLayer = maskLayer;
this.source = source;
this.mode = mode;
this.count = count;
this.offset = offset;
this.patternXY = patternXY;
this.patternWH = patternWH;
this.patternMovement = patternMovement;
this.featureCode = featureCode;
this.tile = tile;
this.parent = parent;
}
/**
* Draw the feature to the GPU
* @param interactive - whether or not the feature is interactive
*/
draw(interactive = false) {
super.draw(interactive);
const { maskLayer, tile, parent, workflow } = this;
const { mask } = parent ?? tile;
// draw
if (maskLayer)
workflow.drawMask(mask);
else
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, maskLayer, source, mode, count, offset, patternXY, patternWH, patternMovement, featureCode, color, opacity, } = this;
const newFeature = new FillFeature(workflow, layerGuide, maskLayer, source, mode, count, offset, patternXY, patternWH, patternMovement, featureCode, tile, parent);
newFeature.setWebGL1Attributes(color, opacity);
return newFeature;
}
/**
* Set the attributes of the feature if the context is webgl1
* @param color - the color
* @param opacity - the opacity
*/
setWebGL1Attributes(color, opacity) {
this.color = color;
this.opacity = opacity;
}
}
/** Fill Workflow */
export default class FillWorkflow extends Workflow {
label = 'fill';
layerGuides = new Map();
/** @param context - The WebGL(1|2) context */
constructor(context) {
// inject Program
super(context);
// get gl from context
const { type } = context;
// build shaders
if (type === 1)
this.buildShaders(vert1, frag1, { aPos: 0, aID: 1, aIndex: 2 });
else
this.buildShaders(vert2, frag2);
}
/**
* Build layer definition for the fill feature
* @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
const { pattern } = layer;
let { color, opacity, invert, opaque, patternFamily, patternMovement, interactive, cursor } = layer;
invert = invert ?? false;
opaque = opaque ?? false;
interactive = interactive ?? false;
cursor = cursor ?? 'default';
color = color ?? 'rgb(0, 0, 0)';
opacity = opacity ?? 1;
patternFamily = patternFamily ?? '__images';
patternMovement = patternMovement ?? false;
// 1) Build layer definition
const layerDefinition = {
...layerBase,
type: 'fill',
// paint
color,
opacity,
// layout
pattern,
patternFamily,
patternMovement,
// properties
invert,
interactive,
opaque,
cursor,
};
// 2) Store layer workflow, building code if webgl2
const layerCode = [];
if (type === 2) {
for (const paint of [color, opacity]) {
layerCode.push(...encodeLayerAttribute(paint, lch));
}
}
// if mask source, and webgl1, build maskColor and maskOpacity
const isGL1Mask = type === 1 && source === 'mask';
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
lch,
invert,
opaque,
interactive,
pattern: pattern !== undefined,
color: isGL1Mask ? parseFeature(color, colorFunc(lch)) : undefined,
opacity: isGL1Mask ? parseFeature(opacity, (i) => [i]) : undefined,
visible,
});
return layerDefinition;
}
/**
* Build a mask feature
* @param maskFeature - the mask feature guide
* @param tile - the tile that needs a mask
*/
buildMaskFeature(maskFeature, tile) {
const { layerIndex, minzoom, maxzoom } = maskFeature;
const { type, gl, layerGuides } = this;
const { zoom, mask } = tile;
// not in the zoom range, ignore
if (zoom < minzoom || zoom > maxzoom)
return;
const layerGuide = layerGuides.get(layerIndex);
if (layerGuide === undefined)
return;
const { color, opacity } = layerGuide;
const feature = new FillFeature(this, layerGuide, true, mask, gl.TRIANGLE_STRIP, mask.count, mask.offset, { x: 0, y: 0 }, [0, 0], 0, [0], tile);
// If webgl1 add color and opacity
if (type === 1) {
feature.setWebGL1Attributes(color?.([], {}, zoom), opacity?.([], {}, zoom));
}
tile.addFeatures([feature]);
}
/**
* Build a fill features from source data sent from the Tile Worker
* @param fillData - the fill data from the Tile Worker
* @param tile - the tile that the features belong to
*/
buildSource(fillData, tile) {
const { gl, context } = this;
const { featureGuideBuffer } = fillData;
// prep buffers
const vertexA = new Float32Array(fillData.vertexBuffer);
const indexA = new Uint32Array(fillData.indexBuffer);
const fillIDA = new Uint8Array(fillData.idBuffer);
const codeTypeA = new Uint8Array(fillData.codeTypeBuffer);
// 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, 0, 2, gl.FLOAT, false, 0, 0);
const indexBuffer = context.bindElementArray(indexA);
const idBuffer = context.bindEnableVertexAttr(fillIDA, 1, 4, gl.UNSIGNED_BYTE, true, 0, 0);
const codeTypeBuffer = context.bindEnableVertexAttr(codeTypeA, 2, 1, gl.UNSIGNED_BYTE, false, 0, 0);
const source = {
type: 'fill',
vertexBuffer,
indexBuffer,
idBuffer,
codeTypeBuffer,
vao,
};
context.finish(); // flush vao
this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer));
}
/**
* Build fill features
* @param source - the fill source
* @param tile - the tile that the features belong to
* @param featureGuideArray - the array of feature guides
*/
#buildFeatures(source, tile, featureGuideArray) {
const { gl } = this;
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;
// If webgl1, we pull out the color and opacity otherwise build featureCode
let featureCode = [0];
let color;
let opacity;
if (this.type === 1) {
color = [];
opacity = [];
for (let s = 0, len = encodingSize / 5; s < len; s++) {
const idx = i + s * 5;
color.push(...featureGuideArray.slice(idx, idx + 4));
opacity.push(featureGuideArray[idx + 4]);
}
if (color.length === 0)
color = undefined;
if (opacity.length === 0)
opacity = undefined;
}
else if (this.type === 2 && encodingSize > 0) {
featureCode = [...featureGuideArray.slice(i, i + encodingSize)];
}
// update index
i += encodingSize;
const [patternX, patternY, patternW, patternH, patternMovement] = featureGuideArray.slice(i, i + 5);
i += 5;
const layerGuide = this.layerGuides.get(layerIndex);
if (layerGuide === undefined)
continue;
if (count > 0) {
const feature = new FillFeature(this, layerGuide, false, source, gl.TRIANGLES, count, offset, { x: patternX, y: patternY }, [patternW, patternH], patternMovement, featureCode, tile);
if (this.type === 1) {
feature.color = color;
feature.opacity = opacity;
}
features.push(feature);
}
}
tile.addFeatures(features);
}
/**
* Draw the fill feature
* @param feature - the fill feature
* @param interactive - whether or not the feature is interactive
*/
draw(feature, interactive = false) {
// grab context
const { gl, context, type, uniforms } = this;
const { uTexSize, uPatternXY, uPatternWH, uPatternMovement, uColors, uOpacity } = uniforms;
const { texture, texSize } = context.sharedFBO;
// get current source data
const { source, tile, parent, count, offset, layerGuide: { layerIndex, visible, invert }, color, opacity, featureCode, mode, patternXY, patternWH, patternMovement, } = feature;
if (!visible)
return;
const { vao } = source;
const { mask } = parent ?? tile;
// ensure proper context state
context.defaultBlend();
context.enableDepthTest();
context.enableCullFace();
context.enableStencilTest();
context.lessDepth();
context.setDepthRange(layerIndex);
// set texture data
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform2fv(uTexSize, texSize);
gl.uniform2fv(uPatternXY, [patternXY.x, patternXY.y]);
gl.uniform2fv(uPatternWH, patternWH);
gl.uniform1i(uPatternMovement, patternMovement);
if (interactive)
context.stencilFuncAlways(0);
if (invert)
gl.colorMask(false, false, false, false);
// set feature code (webgl 1 we store the colors, webgl 2 we store layerCode lookups)
if (type === 1) {
gl.uniform4fv(uColors, color ?? [0, 0, 0, 1]);
gl.uniform1fv(uOpacity, opacity ?? [1]);
}
else {
this.setFeatureCode(featureCode ?? [0]);
}
// draw elements
gl.bindVertexArray(vao);
gl.drawElements(mode, count, gl.UNSIGNED_INT, offset * 4);
// If invert reset color mask & draw a full tile mask
if (invert) {
gl.colorMask(true, true, true, true);
this.drawMask(mask);
}
}
/**
* Draw a mask to the GPU
* @param mask - the tile mask
*/
drawMask(mask) {
const { gl, context } = this;
const { count, offset, vao, tile: { type }, } = mask;
if (type === 'S2')
context.enableCullFace();
else
context.disableCullFace();
// bind vao & draw
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLE_STRIP, count, gl.UNSIGNED_INT, offset * 4);
}
}