s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
329 lines (328 loc) • 15 kB
JavaScript
import { earclip } from 'earclip';
import parseFeature from 's2/style/parseFeature.js';
import parseFilter from 'style/parseFilter.js';
import VectorWorker, { colorFunc, idToRGB } from './vectorWorker.js';
import { featureSort, scaleShiftClipPolys } from './util/index.js';
const MAX_FEATURE_BATCH_SIZE = 1 << 6; // 64
/** Worker for processing fill data */
export default class FillWorker extends VectorWorker {
featureStore = new Map(); // tileID -> features
invertLayers = new Map();
imageStore;
/**
* @param idGen - id generator to ensure features don't overlap
* @param gpuType - the GPU context of the map renderer (WebGL(1|2) | WebGPU)
* @param imageStore - the image store to pull/request the needed pattern images
*/
constructor(idGen, gpuType, imageStore) {
super(idGen, gpuType);
this.imageStore = imageStore;
}
/**
* Setup a fill layer for processing
* @param fillLayer - the fill layer
* @returns the worker layer to process future fill data
*/
setupLayer(fillLayer) {
const { name, layerIndex, source, layer, minzoom, maxzoom, pattern, patternFamily, patternMovement, filter, color, opacity, invert, interactive, cursor, opaque, lch, } = fillLayer;
// build featureCode design
// radius -> opacity
const design = [[color, colorFunc(lch)], [opacity]];
const fillWorkerLayer = {
type: 'fill',
name,
layerIndex,
source,
layer,
minzoom,
maxzoom,
filter: parseFilter(filter),
getCode: this.buildCode(design),
pattern: pattern !== undefined ? parseFeature(pattern) : undefined,
patternFamily: parseFeature(patternFamily),
patternMovement: parseFeature(patternMovement),
invert,
interactive,
cursor,
opaque,
};
if (invert)
this.invertLayers.set(layerIndex, fillWorkerLayer);
return fillWorkerLayer;
}
/**
* Build a fill feature from input vector features
* @param tile - the tile request
* @param extent - the tile extent
* @param feature - the vector tile feature
* @param fillLayer - the fill worker layer
* @param mapID - the map id to ship the data back to
* @param sourceName - the source name the data to belongs to
* @returns true if the feature was built
*/
async buildFeature(tile, extent, feature, fillLayer, mapID, sourceName) {
const { gpuType, imageStore } = this;
// pull data
const { zoom, division } = tile;
const { properties } = feature;
const { getCode, interactive, layerIndex } = fillLayer;
const type = feature.geoType();
// only accept polygons and multipolygons
if (type !== 'Polygon' && type !== 'MultiPolygon')
return false;
// get pattern
const pattern = fillLayer.pattern?.([], properties, zoom);
const patternFamily = fillLayer.patternFamily([], properties, zoom);
const patternMovement = fillLayer.patternMovement([], properties, zoom);
let missing = false;
if (pattern !== undefined) {
await imageStore.getReady(mapID);
missing = imageStore.addMissingGlyph(mapID, tile.id, [pattern], [patternFamily]);
}
const hasParent = tile.parent !== undefined;
const [geometry, indices] = !hasParent && 'loadGeometryFlat' in feature ? feature.loadGeometryFlat() : [[], []];
let vertices = [];
if (geometry === undefined)
return false;
// if not parent and indices, the polygon has already been "solved"
if (hasParent || indices.length === 0) {
const [geometry] = feature.loadPolys() ?? [];
if (geometry === undefined)
return false;
// preprocess geometry
const clip = scaleShiftClipPolys(geometry, extent, tile);
// create multiplier
const multiplier = 1 / extent;
// process
for (const poly of clip) {
// create triangle mesh
const data = earclip(poly, extent / division, vertices.length / 2);
// store vertices
for (let i = 0, vl = data.vertices.length; i < vl; i++) {
vertices.push(data.vertices[i] * multiplier);
}
// store indices
for (let i = 0, il = data.indices.length; i < il; i++) {
indices.push(data.indices[i]);
}
}
}
else {
vertices = geometry;
}
// if geometry is empty, skip
if (vertices.length === 0 || indices.length === 0)
return false;
const id = !isNaN(properties.__id) ? Number(properties.__id) : this.idGen.getNum();
const [gl1Code, gl2Code] = getCode(zoom, properties);
const fillFeature = {
vertices,
indices,
layerIndex,
code: gpuType === 1 ? gl1Code : gl2Code,
gl2Code,
pattern,
patternFamily,
patternMovement,
idRGB: idToRGB(id),
missing,
};
// if interactive, store interactive properties
if (interactive)
this._addInteractiveFeature(id, properties, fillLayer);
const storeID = `${mapID}:${tile.id}:${sourceName}`;
if (!this.featureStore.has(storeID))
this.featureStore.set(storeID, []);
const features = this.featureStore.get(storeID);
features?.push(fillFeature);
return true;
}
/**
* Flush the fill feature data to be shipped out
* @param mapID - id of the map to ship the data back to
* @param tile - tile request
* @param sourceName - source name the data to belongs to
* @param wait - this promise must be resloved before flushing. Ensures pattern data is ready
*/
async flush(mapID, tile, sourceName, wait) {
const storeID = `${mapID}:${tile.id}:${sourceName}`;
const features = this.featureStore.get(storeID) ?? [];
// If `invertLayers` is non-empty, we should check if `features`
// does not have said invert layers. If it doesn't, we need to add
// a dummy feature that is empty for said layers.
for (const [layerIndex, fillWorkerLayer] of this.invertLayers) {
if (fillWorkerLayer.source !== sourceName)
continue;
if (!features.some((feature) => feature.layerIndex === layerIndex)) {
const feature = await this.#buildInvertFeature(tile, fillWorkerLayer, mapID, sourceName);
if (feature !== undefined)
features.push(feature);
}
}
if (features.length !== 0) {
// check if we need to wait for a response of missing data
const missing = features.some((feature) => feature.missing);
if (missing)
await wait;
this.#flush(mapID, sourceName, tile.id);
}
// finish the flush
await super.flush(mapID, tile, sourceName, wait);
this.featureStore.delete(storeID);
}
/**
* Build inverted features if necessary
* NOTE: You can not build invert features that require properties data
* @param tile - tile request
* @param fillWorkerLayer - the fill worker layer that guides the feature processing
* @param mapID - the map id that the feature belongs to
* @param sourceName - the source name that the feature belongs to
* @returns the inverted fill data to be rendered if it exists
*/
async #buildInvertFeature(tile, fillWorkerLayer, mapID, sourceName) {
const { gpuType, imageStore } = this;
const { zoom } = tile;
const { getCode, minzoom, maxzoom, layerIndex } = fillWorkerLayer;
// respect zoom range
if (zoom < minzoom || zoom > maxzoom)
return;
// get pattern
const pattern = fillWorkerLayer.pattern?.([], {}, zoom);
const patternFamily = fillWorkerLayer.patternFamily([], {}, zoom);
const patternMovement = fillWorkerLayer.patternMovement([], {}, zoom);
// get if missing
let missing = false;
if (pattern !== undefined) {
await imageStore.getReady(mapID);
missing = imageStore.addMissingGlyph(mapID, tile.id, [pattern], [patternFamily]);
}
// build feature
const id = this.idGen.getNum();
const [gl1Code, gl2Code] = getCode(zoom, {});
const feature = {
vertices: [-0.1, -0.1, 1.1, -0.1, 1.1, 1.1, -0.1, 1.1],
indices: [0, 2, 1, 2, 0, 3],
layerIndex,
code: gpuType === 1 ? gl1Code : gl2Code,
gl2Code,
pattern,
patternFamily,
patternMovement,
idRGB: idToRGB(id),
missing,
};
const storeID = `${mapID}:${tile.id}:${sourceName}`;
if (!this.featureStore.has(storeID))
this.featureStore.set(storeID, []);
const features = this.featureStore.get(storeID);
features?.push(feature);
return feature;
}
/**
* Flush the fill data to the main thread
* @param mapID - map id to ship the data back to
* @param sourceName - source name the data to belongs to
* @param tileID - tile id the data to belongs to
*/
#flush(mapID, sourceName, tileID) {
const storeID = `${mapID}:${tileID}:${sourceName}`;
const features = this.featureStore.get(storeID) ?? [];
if (features.length === 0)
return;
// now that we have created all triangles, let's merge into bundled buffer sets
// for the main thread to build VAOs.
// Step 1: Sort by layerIndex, than sort by feature code.
features.sort(featureSort);
// step 2: Run through all features and bundle into the fewest featureBatches. Caveats:
// 1) don't store VAO set larger than index size (we use an extension for WebGL1, so we will probably never go over 1 << 32)
// 2) don't store any feature code larger than MAX_FEATURE_BATCH_SIZE
const vertices = [];
const indices = [];
const ids = [];
const codeType = [];
const featureGuide = [];
let encodings = [];
let indicesOffset = 0;
let vertexOffset = 0;
let encodingIndexes = { '': 0 };
let encodingIndex = 0;
let curlayerIndex = features[0].layerIndex;
let curPattern = features[0].pattern;
let curPatternFamily = features[0].patternFamily;
let curPatternMovement = features[0].patternMovement;
for (const { code, layerIndex, vertices: _vertices, indices: _indices, idRGB, pattern, patternFamily, patternMovement, } of features) {
// on layer change or max encoding size, we have to setup a new featureGuide, encodings, and encodingIndexes
if (curlayerIndex !== layerIndex || encodings.length + code.length > MAX_FEATURE_BATCH_SIZE) {
const indexSize = indices.length - indicesOffset;
if (indexSize === 0)
continue; // skip if no indices
// only store if count is actually greater than 0
featureGuide.push(curlayerIndex, indexSize, indicesOffset, encodings.length, ...encodings); // layerIndex, count, offset, encoding size, encodings
// describe pattern
const { texX, texY, texW, texH } = this.imageStore.getPattern(mapID, patternFamily, pattern);
featureGuide.push(texX, texY, texW, texH, patternMovement ? 1 : 0);
// update variables for reset
indicesOffset = indices.length;
encodings = [];
encodingIndexes = { '': 0 };
}
// setup encodings data. If we didn't have current feature's encodings already, create and set index
const feKey = code.toString();
encodingIndex = encodingIndexes[feKey];
if (encodingIndex === undefined) {
encodingIndex = encodingIndexes[feKey] =
this.gpuType === 1 ? encodings.length / 5 : encodings.length;
encodings.push(...code);
}
// store
vertexOffset = vertices.length / 2;
// NOTE: Spreader functions on large arrays are failing in chrome right now -_-
// so we just do a for loop
for (let f = 0, fl = _vertices.length; f < fl; f++) {
vertices.push(_vertices[f]);
}
for (let f = 0, fl = _indices.length; f < fl; f++) {
const index = _indices[f] + vertexOffset;
indices.push(index);
codeType[index] = encodingIndex;
// store id RGB value
const idRGBIndex = index * 4;
ids[idRGBIndex] = idRGB[0];
ids[idRGBIndex + 1] = idRGB[1];
ids[idRGBIndex + 2] = idRGB[2];
ids[idRGBIndex + 3] = 0;
}
// update previous layerIndex and pattern
curlayerIndex = layerIndex;
curPattern = pattern;
curPatternFamily = patternFamily;
curPatternMovement = patternMovement;
}
// store the very last featureGuide batch
if (indices.length - indicesOffset > 0) {
featureGuide.push(curlayerIndex, indices.length - indicesOffset, indicesOffset, encodings.length, ...encodings); // layerIndex, count, offset, encoding size, encodings
// describe pattern
const { texX, texY, texW, texH } = this.imageStore.getPattern(mapID, curPatternFamily, curPattern);
featureGuide.push(texX, texY, texW, texH, curPatternMovement ? 1 : 0);
}
// Upon building the batches, convert to buffers and ship.
const vertexBuffer = new Float32Array(vertices).buffer;
const indexBuffer = new Uint32Array(indices).buffer;
const idBuffer = new Uint8ClampedArray(ids).buffer; // pre-store each id as an rgb value
const codeTypeBuffer = this.gpuType === 3 ? new Uint32Array(codeType).buffer : new Uint8Array(codeType).buffer;
const featureGuideBuffer = new Float32Array(featureGuide).buffer;
// ship the vector data.
const message = {
mapID,
type: 'fill',
sourceName,
tileID,
vertexBuffer,
indexBuffer,
idBuffer,
codeTypeBuffer,
featureGuideBuffer,
};
postMessage(message, [vertexBuffer, indexBuffer, idBuffer, codeTypeBuffer, featureGuideBuffer]);
}
}