s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
258 lines (257 loc) • 10.2 kB
JavaScript
import { Feature } from './workflow.js';
import { buildColorRamp } from 'style/color/index.js';
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
// WEBGL1
import frag1 from '../shaders/sensors1.fragment.glsl';
import vert1 from '../shaders/sensors1.vertex.glsl';
// WEBGL2
import frag2 from '../shaders/sensors2.fragment.glsl';
import vert2 from '../shaders/sensors2.vertex.glsl';
/** Sensor Feature is a standalone sensor render storage unit that can be drawn to the GPU */
export class SensorFeature extends Feature {
layerGuide;
workflow;
featureCode;
tile;
fadeStartTime;
parent;
type = 'sensor';
opacity; // webgl1
/**
* @param layerGuide - sensor layer guide for this feature
* @param workflow - the sensor workflow
* @param featureCode - the encoded feature code that tells the GPU how to compute it's properties
* @param tile - the tile that the feature is drawn on
* @param fadeStartTime - the start time of the "fade" to be applied
* @param parent - the parent tile
*/
constructor(layerGuide, workflow, featureCode, tile, fadeStartTime = Date.now(), parent) {
super(workflow, tile, layerGuide, featureCode, parent);
this.layerGuide = layerGuide;
this.workflow = workflow;
this.featureCode = featureCode;
this.tile = tile;
this.fadeStartTime = fadeStartTime;
this.parent = parent;
}
/**
* Draw this feature to the GPU
* @param interactive - whether or not the feature is interactive for compute or render
*/
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, featureCode, fadeStartTime, opacity } = this;
const newFeature = new SensorFeature(layerGuide, workflow, featureCode, tile, fadeStartTime, parent);
newFeature.setWebGL1Attributes(opacity);
return newFeature;
}
/**
* Get the sensor textures
* @returns the sensor textures
*/
getTextures() {
const { tile: { id }, workflow: { timeCache }, layerGuide: { sourceName }, } = this;
return timeCache?.getTextures(id, sourceName) ?? {};
}
/**
* Set the webgl1 attributes if the context is webgl1
* @param opacity - the opacity
*/
setWebGL1Attributes(opacity) {
this.opacity = opacity;
}
}
/**
* Build the sensor workflow. This workflow is added to the painter modularly
* @param context - The WebGL(1|2) context
* @returns the sensor workflow
*/
export default async function sensorWorkflow(context) {
const Workflow = await import('./workflow.js').then((m) => m.default);
/** Sensor Workflow that draws sensor features for WebGL(1|2) */
class SensorWorkflow extends Workflow {
label = 'sensor';
nullTexture;
timeCache;
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 { uColorRamp, uImage, uNextImage } = this.uniforms;
gl.uniform1i(uColorRamp, 0);
gl.uniform1i(uImage, 1);
gl.uniform1i(uNextImage, 2);
// set a null texture
this.#createNullTexture();
}
/** Create a null texture for cases where a texture doesn't exist or is null */
#createNullTexture() {
const { gl } = this;
const texture = gl.createTexture();
if (texture === null)
throw new Error('Failed to create sensor null texture');
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
this.nullTexture = texture;
}
/**
* Inject a time cache as the current cache for the sensor workflow
* @param timeCache - the time cache
*/
injectTimeCache(timeCache) {
this.timeCache = timeCache;
}
/**
* Build sensor source data
* @param sensorData - the input sensor data
* @param tile - the tile we are building the features for
*/
buildSource(sensorData, tile) {
const { gl, context } = this;
const { image, sourceName, size, time } = sensorData;
// do not premultiply
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
// setup texture params
const texture = context.buildTexture(image, size);
// Extend mask
const sensorSource = {
texture,
};
// inject source into timeCache
this.timeCache?.addSourceData(tile.id, time, sourceName, sensorSource);
this.#buildFeatures(sensorData, tile);
}
/**
* Build sensor features
* @param rasterData - the input sensor data
* @param tile - the tile we are building the features for
*/
#buildFeatures(rasterData, tile) {
const { featureGuides } = rasterData;
const features = [];
// for each layer that maches the source, build the feature
for (const { code, layerIndex } of featureGuides) {
const layerGuide = this.layerGuides.get(layerIndex);
if (layerGuide === undefined)
continue;
const feature = new SensorFeature(layerGuide, this, [0], tile);
if (this.type === 1)
feature.setWebGL1Attributes(code[0]);
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 sensor feature
*/
buildLayerDefinition(layerBase, layer) {
const { source, layerIndex, lch, visible, interactive } = layerBase;
// PRE) get layer properties
const { cursor } = layer;
let { colorRamp, opacity, fadeDuration } = layer;
opacity = opacity ?? 1;
colorRamp = colorRamp ?? 'sinebow';
fadeDuration = fadeDuration ?? 300;
// 1) build definition
const layerDefinition = {
...layerBase,
type: 'sensor',
opacity,
colorRamp,
fadeDuration,
interactive: interactive ?? false,
cursor: cursor ?? 'default',
};
// 2) Store layer workflow, building code if webgl2
const layerCode = [];
layerCode.push(...encodeLayerAttribute(opacity, lch));
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
lch,
fadeDuration,
colorRamp: context.buildTexture(buildColorRamp(colorRamp, lch), 256, 4),
visible,
interactive: interactive ?? false,
opaque: false,
});
return layerDefinition;
}
/** Use this workflow as the current shaders for the GPU */
use() {
super.use();
context.oneBlend();
context.enableDepthTest();
context.enableCullFace();
context.enableStencilTest();
context.lessDepth();
}
/**
* Draw a sensor feature
* @param feature - the feature to draw
* @param _interactive - whether or not the feature is interactive
*/
draw(feature, _interactive = false) {
// grab gl from the context
const { gl, type, context, nullTexture, uniforms } = this;
const { uTime, uOpacity } = uniforms;
// get current source data. Time is a uniform
const { tile, parent, featureCode, opacity, layerGuide: { layerIndex, visible, colorRamp }, } = feature;
if (!visible)
return;
const { time, texture, textureNext } = feature.getTextures();
const { mask } = parent ?? tile;
const { vao, count, offset } = mask;
if (time === undefined || texture === undefined)
return;
context.setDepthRange(layerIndex);
// set feature code (webgl 1 we store the opacity, webgl 2 we store layerCode lookups)
if (type === 1) {
gl.uniform1f(uOpacity, opacity ?? 1);
}
else {
this.setFeatureCode(featureCode);
}
// set time uniform
gl.uniform1f(uTime, time);
// setup the textures
gl.activeTexture(gl.TEXTURE2); // uNextImage
if (textureNext !== undefined)
gl.bindTexture(gl.TEXTURE_2D, textureNext);
else
gl.bindTexture(gl.TEXTURE_2D, nullTexture);
gl.activeTexture(gl.TEXTURE1); // uImage
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.activeTexture(gl.TEXTURE0); // uColorRamp
gl.bindTexture(gl.TEXTURE_2D, colorRamp);
// draw elements
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLE_STRIP, count, gl.UNSIGNED_INT, offset);
}
}
return new SensorWorkflow(context);
}