UNPKG

s2maps-gpu

Version:

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

247 lines (246 loc) 9.69 kB
import { WebGPUContext } from './context/index.js'; import { FillWorkflow, GlyphWorkflow, HeatmapWorkflow, HillshadeWorkflow, LineWorkflow, PointWorkflow, RasterWorkflow, ShadeWorkflow, SkyboxWorkflow, WallpaperWorkflow, } from './workflows/index.js'; /** * # GPU Painter * * ## Description * The GPU painter is the main entry point to the GPU features. */ export default class Painter { context; workflows = {}; dirty = true; /** * @param context - the GPU context wrapper * @param options - map options to pull out the options that impact the painter and GPU */ constructor(context, options) { this.context = new WebGPUContext(context, options, this); } /** called once to properly prepare the context */ async prepare() { await this.context.connectGPU(); } /** * Given a tile, build the feature data associated with it * @param tile - the tile to inject the features into * @param data - the collection of data to sift through and build features */ buildFeatureData(tile, data) { const workflow = this.workflows[data.type]; workflow?.buildSource?.(data, tile); } /** * Build all workflows used by the style layers * @param buildSet - the set of workflows to build */ async buildWorkflows(buildSet) { const { workflows, context } = this; const workflowCases = { /** @returns a Fill Workflow */ fill: () => new FillWorkflow(context), /** @returns a raster Workflow */ raster: () => new RasterWorkflow(context), /** @returns a sensor Workflow (eventually) */ sensor: () => new RasterWorkflow(context), /** @returns a line Workflow */ line: () => new LineWorkflow(context), /** @returns a point Workflow */ point: () => new PointWorkflow(context), /** @returns a heatmap Workflow */ heatmap: () => new HeatmapWorkflow(context), /** @returns a hillshade Workflow */ hillshade: () => new HillshadeWorkflow(context), /** @returns a shade Workflow */ shade: () => new ShadeWorkflow(context), /** @returns a glyph Workflow */ glyph: () => new GlyphWorkflow(context), /** @returns a wallpaper Workflow */ wallpaper: () => new WallpaperWorkflow(context), /** @returns a skybox Workflow */ skybox: () => new SkyboxWorkflow(context), }; const promises = []; for (const set of buildSet) { // @ts-expect-error - we know its a workflow const workflow = (workflows[set] = workflowCases[set]()); if (set === 'wallpaper' || set === 'skybox') workflows.background = workflows[set]; promises.push(workflow.setup()); } await Promise.allSettled(promises); } /** @returns a Uint8ClampedArray screen capture */ async getScreen() { return await this.context.getRenderData(); } /** * Set the colorblind mode * @param mode - colorblind mode to set */ setColorMode(mode) { this.dirty = true; this.context.setColorBlindMode(mode); } /** * Inject a glyph image to the GPU * @param maxHeight - the maximum height of the texture * @param images - the glyph images * @param tiles - the tiles to update */ injectGlyphImages(maxHeight, images, tiles) { const textureResized = this.context.injectImages(maxHeight, images); if (textureResized) { for (const feature of tiles.flatMap((tile) => tile.featureGuides)) feature.updateSharedTexture?.(); } } /** * Inject a sprite image to the GPU * @param data - the raw image data of the sprite * @param tiles - the tiles to update */ injectSpriteImage(data, tiles) { const textureResized = this.context.injectSpriteImage(data); if (textureResized) { for (const feature of tiles.flatMap((tile) => tile.featureGuides)) feature.updateSharedTexture?.(); } } /** * Inject a time cache for the sensor workflow * @param timeCache - the time cache to inject */ injectTimeCache(timeCache) { this.workflows.sensor?.injectTimeCache(timeCache); } /** * Resize the canvas * @param width - new width * @param height - new height */ resize(width, height) { this.context.resize(() => { // If any workflows are using the resize method, call it for (const workflow of Object.values(this.workflows)) workflow.resize?.(width, height); }); // notify that the painter is dirty this.dirty = true; } /** * Paint all the tiles in view * @param projector - the camera and what it currently sees * @param tiles - all the tiles in view to paint */ paint(projector, tiles) { const { context, workflows } = this; // setup for the next frame context.newScene(projector.view, projector.getMatrix('m')); // prep mask id's tiles.forEach((tile, index) => { tile.tmpMaskID = index + 1; }); const allFeatures = tiles.flatMap((tile) => tile.featureGuides); // Mercator: the tile needs to update it's matrix at all zooms. // S2: all features tiles past zoom 12 must set screen positions let featureTiles = allFeatures.flatMap(({ parent, tile }) => parent !== undefined ? [parent, tile] : [tile]); // remove all duplicates of tiles by their id featureTiles = featureTiles.filter((tile, index) => featureTiles.findIndex((t) => t.id === tile.id) === index); for (const tile of featureTiles) tile.setScreenPositions(projector); // prep all tile's features to draw const features = allFeatures.filter((f) => f.type !== 'heatmap'); // draw heatmap data if applicable, and a singular feature for the main render thread to draw the texture to the screen const heatmapFeatures = allFeatures.filter((f) => f.type === 'heatmap'); // compute heatmap data const heatmapFeature = workflows.heatmap?.textureDraw(heatmapFeatures); if (heatmapFeature !== undefined) features.push(...heatmapFeature); // sort features features.sort(featureSort); // prep glyph features for drawing box filters const glyphFeatures = features.filter((f) => f.type === 'glyph'); workflows.glyph?.computeFilters(glyphFeatures); // DRAW PHASE // draw masks for (const { mask } of tiles) mask.draw(); // draw the wallpaper workflows.background?.draw(projector); // paint opaque fills const opaqueFillFeatures = features.filter((f) => f.layerGuide.opaque).reverse(); for (const feature of opaqueFillFeatures) feature.draw(); // paint features that are potentially transparent const residualFeatures = features.filter((f) => !(f.layerGuide.opaque ?? false)); for (const feature of residualFeatures) feature.draw(); // finish context.finish(); } /** * Compute the interactive features in current view * @param tiles - current view tiles that we need to sift through for interactive features */ computeInteractive(tiles) { const interactiveFeatures = tiles .flatMap((tile) => tile.featureGuides) .filter(({ layerGuide }) => layerGuide.interactive) .sort(featureSort) .reverse(); if (interactiveFeatures.length > 0) { // prepare & compute this.context.clearInteractBuffer(); this.#computeInteractive(interactiveFeatures); } } /** * Compute the interactive features via the GPU * @param features - input features that need to be computed */ #computeInteractive(features) { const { device, frameBufferBindGroup } = this.context; // prepare const commandEncoder = device.createCommandEncoder(); const computePass = (this.context.computePass = commandEncoder.beginComputePass()); computePass.setBindGroup(0, frameBufferBindGroup); // compute for (const feature of features) feature.compute?.(); // finish computePass.end(); device.queue.submit([commandEncoder.finish()]); } /** Delete the GPU instance */ delete() { const { context, workflows } = this; for (const workflow of Object.values(workflows)) workflow.destroy(); context.destroy(); } } /** * Sort the features * @param a - first feature * @param b - comparison feature * @returns a negative value if a < b, 0 if a === b, and a positive value if a > b */ function featureSort(a, b) { // first check if the layer is the same let diff = a.layerGuide.layerIndex - b.layerGuide.layerIndex; if (diff !== 0) return diff; // check for zoom difference const zoomDiff = (a.parent !== undefined ? 1 : 0) - (b.parent !== undefined ? 1 : 0); if (zoomDiff !== 0) return zoomDiff; // lastlye try to sort by feature code let index = 0; const maxSize = Math.min(a.featureCode.length, b.featureCode.length); while (diff === 0 && index < maxSize) { diff = a.featureCode[index] - b.featureCode[index]; index++; } return diff; }