s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
289 lines (288 loc) • 11.5 kB
JavaScript
/** CONTEXTS */
import { WebGL2Context, WebGLContext } from './context/index.js';
/** WORKFLOWS */
import { FillWorkflow, GlyphFilterWorkflow, GlyphWorkflow, HeatmapWorkflow, HillshadeWorkflow, LineWorkflow, PointWorkflow, RasterWorkflow, ShadeWorkflow, SkyboxWorkflow, WallpaperWorkflow, } from './workflows/index.js';
/**
* # WebGL(1|2) Painter
*
* ## Description
* A painter for WebGL(1|2) contexts
*/
export default class Painter {
context;
workflows = {};
curWorkflow;
dirty = true;
/**
* @param context - a WebGL(1|2) context wrapper
* @param type - 1 for WebGL 1, 2 for WebGL 2
* @param options - map options to pull out the options that impact the painter and GPU
*/
constructor(context, type, options) {
// build a context API
if (type === 2)
this.context = new WebGL2Context(context, options, this);
else
this.context = new WebGLContext(context, options, this);
}
/** called once to properly prepare the context */
async prepare() { }
/**
* 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 promises = [];
const workflowImports = {
/** @returns a fill rendering tool */
fill: async () => await new FillWorkflow(context),
/** @returns a glyph rendering tool */
glyphFilter: async () => await new GlyphFilterWorkflow(context),
/** @returns a glyph rendering tool */
glyph: async () => await new GlyphWorkflow(context),
/** @returns a heatmap rendering tool */
heatmap: async () => await new HeatmapWorkflow(context),
/** @returns a hillshade rendering tool */
hillshade: async () => await new HillshadeWorkflow(context),
/** @returns a line rendering tool */
line: async () => await new LineWorkflow(context),
/** @returns a point rendering tool */
point: async () => await new PointWorkflow(context),
/** @returns a raster rendering tool */
raster: async () => await new RasterWorkflow(context),
/** @returns a sensor rendering tool */
sensor: async () => await import('./workflows/sensorWorkflow.js'),
/** @returns a shade rendering tool */
shade: async () => await new ShadeWorkflow(context),
/** @returns a skybox rendering tool */
skybox: async () => await new SkyboxWorkflow(context),
/** @returns a wallpaper rendering tool */
wallpaper: async () => await new WallpaperWorkflow(context),
};
const workflowKeys = [];
for (const workflow of buildSet) {
if (workflow in workflows)
continue;
if (workflow === 'glyph')
workflowKeys.push('glyphFilter');
workflowKeys.push(workflow);
}
// actually import the workflows
for (const key of workflowKeys) {
promises.push(workflowImports[key]?.()
.then(async (res) => {
if ('default' in res) {
const { default: pModule } = res;
workflows[key] = await pModule(context);
}
else {
workflows[key] = res;
}
if (key === 'wallpaper' || key === 'skybox')
workflows.background = workflows[key];
})
.catch((err) => {
console.error(`FAILED to import painter workflow ${key}`, err);
}));
}
await Promise.allSettled(promises);
if (workflows.glyphFilter !== undefined && workflows.glyph !== undefined) {
const glyph = workflows.glyph;
glyph.injectFilter(workflows.glyphFilter);
}
}
/**
* Inject frame uniforms. WebGL requires uniforms to be update before each draw
* @param matrix - the projection matrix
* @param view - the view matrix
* @param aspect - the canvas aspect ratio
*/
injectFrameUniforms(matrix, view, aspect) {
const { workflows } = this;
for (const workflowName in workflows) {
workflows[workflowName]?.injectFrameUniforms(matrix, view, aspect);
}
}
/**
* 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) {
const { context } = this;
// If we are using the text workflow, update the text workflow's framebuffer component's sizes
const glyphFilter = this.workflows.glyphFilter;
const heatmap = this.workflows.heatmap;
if (glyphFilter !== undefined)
glyphFilter.resize();
if (heatmap !== undefined)
heatmap.resize();
// ensure interaction buffer is accurate
context.resize();
// ensure our default viewport is accurate
context.resetViewport();
// 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;
// reset the current workflow as undefined to ensure a new flush happens
context.resetWorkflow();
// prep frame uniforms
const { view, aspect } = projector;
const matrix = projector.getMatrix('m');
this.injectFrameUniforms(matrix, view, aspect);
// 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 hfs = workflows.heatmap?.textureDraw(heatmapFeatures);
if (hfs !== undefined)
features.push(...hfs);
// 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
// setup for the next frame
context.newScene();
// draw masks
context.enableMaskTest();
for (const { mask } of tiles)
mask.draw();
context.flushMask();
// 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((feature) => feature.layerGuide.interactive)
.sort(featureSort)
.reverse();
if (interactiveFeatures.length > 0) {
// prepare & compute
this.context.clearInteractBuffer();
for (const f of interactiveFeatures)
f.draw(true);
}
}
/** @returns a Uint8ClampedArray of the current screen */
async getScreen() {
const { gl } = this.context;
const { canvas, RGBA, UNSIGNED_BYTE } = gl;
const { width, height } = canvas;
const pixels = new Uint8ClampedArray(width * height * 4);
gl.readPixels(0, 0, width, height, RGBA, UNSIGNED_BYTE, pixels);
return await pixels;
}
/**
* Inject a glyph image to the GPU
* @param maxHeight - the maximum height of the texture
* @param images - the glyph images
*/
injectGlyphImages(maxHeight, images) {
this.context.injectImages(maxHeight, images);
}
/**
* Inject a sprite image to the GPU
* @param data - the raw image data of the sprite
*/
injectSpriteImage(data) {
this.context.injectSpriteImage(data);
}
/**
* Set the colorblind mode
* @param mode - colorblind mode to set
*/
setColorMode(mode) {
this.dirty = true;
// tell all the workflows
const { workflows } = this;
for (const workflowName in workflows) {
const workflow = workflows[workflowName];
workflow.updateColorBlindMode = mode;
}
}
/** Delete the GPU and painter instance */
delete() {
const { context, workflows } = this;
for (const workflow of Object.values(workflows))
workflow.delete();
context.delete();
}
}
/**
* 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;
}