s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
265 lines (264 loc) • 9.93 kB
JavaScript
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
import Workflow, { Feature } from './workflow.js';
// WEBGL1
import frag1 from '../shaders/hillshade1.fragment.glsl';
import vert1 from '../shaders/hillshade1.vertex.glsl';
// WEBGL2
import frag2 from '../shaders/hillshade2.fragment.glsl';
import vert2 from '../shaders/hillshade2.vertex.glsl';
/** Hillshade Feature is a standalone hillshade render storage unit that can be drawn to the GPU */
export class HilllshadeFeature extends Feature {
workflow;
layerGuide;
tile;
source;
fadeStartTime;
parent;
type = 'hillshade';
opacity; // webgl1
shadowColor; // webgl1
accentColor; // webgl1
highlightColor; // webgl1
azimuth; // webgl1
altitude; // webgl1
/**
* @param workflow - the hillshade workflow
* @param layerGuide - layer guide for this feature
* @param tile - the tile that the feature is drawn on
* @param source - the raster source
* @param fadeStartTime - the start time of the fade
* @param parent - the parent tile
*/
constructor(workflow, layerGuide, tile, source, fadeStartTime = Date.now(), parent) {
super(workflow, tile, layerGuide, [0], parent);
this.workflow = workflow;
this.layerGuide = layerGuide;
this.tile = tile;
this.source = source;
this.fadeStartTime = fadeStartTime;
this.parent = parent;
}
/**
* Draw the feature to the GPU
* @param interactive - whether or not the feature is interactive
*/
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, source, fadeStartTime, opacity, shadowColor, accentColor, highlightColor, azimuth, altitude, } = this;
const newFeature = new HilllshadeFeature(workflow, layerGuide, tile, source, fadeStartTime, parent);
newFeature.setWebGL1Attributes(opacity, shadowColor, accentColor, highlightColor, azimuth, altitude);
return newFeature;
}
/**
* Set the attributes of the feature if the context is webgl1
* @param opacity - the opacity
* @param shadowColor - the shadow color
* @param accentColor - the accent color
* @param highlightColor - the highlight color
* @param azimuth - the azimuth
* @param altitude - the altitude
*/
setWebGL1Attributes(opacity, shadowColor, accentColor, highlightColor, azimuth, altitude) {
this.opacity = opacity;
this.shadowColor = shadowColor;
this.accentColor = accentColor;
this.highlightColor = highlightColor;
this.azimuth = azimuth;
this.altitude = altitude;
}
/**
* Set the attributes of the feature if the context is webgl1
* @param code - the code
*/
setWebGL1AttributesCode(code) {
this.setWebGL1Attributes(code[0], code.slice(1, 5), code.slice(5, 9), code.slice(9, 13), code[13], code[14]);
}
}
/** Hillshade Workflow */
export default class HillshadeWorkflow extends Workflow {
label = 'hillshade';
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 { uTexture } = this.uniforms;
gl.uniform1i(uTexture, 0);
}
/**
* Build the hillshade source
* @param hillshadeData - the hillshade data sent from the Tile Worker
* @param tile - the tile that the feature is drawn on
*/
buildSource(hillshadeData, tile) {
const { gl, context } = this;
const { image, size } = hillshadeData;
// do not premultiply
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
// setup texture params
const texture = context.buildTexture(image, size);
// create the soruce
const source = { type: 'raster', texture, size };
// build features
this.#buildFeatures(source, hillshadeData, tile);
}
/**
* Build the hillshade features
* @param source - the source
* @param hillshadeData - the hillshade data
* @param tile - the tile that the features are drawn on
*/
#buildFeatures(source, hillshadeData, tile) {
const { featureGuides } = hillshadeData;
// for each layer that maches the source, build the feature
const features = [];
for (const { code, layerIndex } of featureGuides) {
const layerGuide = this.layerGuides.get(layerIndex);
if (layerGuide === undefined)
continue;
const feature = new HilllshadeFeature(this, layerGuide, tile, source);
if (this.type === 1)
feature.setWebGL1AttributesCode(code);
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 feature
*/
buildLayerDefinition(layerBase, layer) {
const { type } = this;
const { source, layerIndex, lch, visible, interactive } = layerBase;
// PRE) get layer properties
let { unpack, shadowColor, accentColor, highlightColor, opacity, azimuth, altitude, fadeDuration, } = layer;
shadowColor = shadowColor ?? '#000';
accentColor = accentColor ?? '#000';
highlightColor = highlightColor ?? '#fff';
opacity = opacity ?? 1;
azimuth = azimuth ?? 315;
altitude = altitude ?? 45;
fadeDuration = fadeDuration ?? 300;
// defaults to mapbox unpack
unpack = unpack ?? {
offset: -10000,
zFactor: 0.1,
aMultiplier: 0,
bMultiplier: 1,
gMultiplier: 256,
rMultiplier: 256 * 256,
};
// 1) build definition
const layerDefinition = {
...layerBase,
type: 'hillshade',
shadowColor,
accentColor,
highlightColor,
azimuth,
altitude,
opacity,
unpack,
};
// 2) Store layer workflow, building code if webgl2
const layerCode = [];
if (type === 2) {
for (const paint of [opacity, shadowColor, accentColor, highlightColor, azimuth, altitude]) {
layerCode.push(...encodeLayerAttribute(paint, lch));
}
}
// 3) Store layer guide
const unpackData = [
unpack.offset,
unpack.zFactor,
unpack.rMultiplier,
unpack.gMultiplier,
unpack.bMultiplier,
unpack.aMultiplier,
];
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
lch,
fadeDuration,
unpack: unpackData,
visible,
interactive: interactive ?? false,
opaque: false,
});
return layerDefinition;
}
/** Use this workflow as the current shaders for the GPU */
use() {
super.use();
const { context } = this;
// setup context
context.defaultBlend();
context.enableDepthTest();
context.enableCullFace();
context.enableStencilTest();
context.lessDepth();
}
/**
* Draw the hillshade feature
* @param feature - the feature guide
* @param _interactive - whether or not the feature is interactive
*/
draw(feature, _interactive = false) {
// grab gl from the context
const { type, gl, context, uniforms } = this;
const { uFade, uTexLength, uUnpack, uOpacity, uShadowColor, uAccentColor, uHighlightColor, uAzimuth, uAltitude, } = uniforms;
const { PI, min, max } = Math;
// get current source data
const { tile, parent, source, layerGuide: { layerIndex, visible, unpack }, featureCode, opacity, shadowColor, accentColor, highlightColor, azimuth, altitude, } = feature;
if (!visible)
return;
const { texture, size } = source;
const { vao, count, offset } = (parent ?? tile).mask;
context.setDepthRange(layerIndex);
// set fade
gl.uniform1f(uFade, 1);
gl.uniform1fv(uUnpack, unpack);
// set feature code (webgl 1 we store the opacity, webgl 2 we store layerCode lookups)
if (type === 1) {
gl.uniform1f(uTexLength, size);
gl.uniform1f(uOpacity, opacity ?? 1);
gl.uniform4fv(uShadowColor, shadowColor ?? [0, 0, 0, 1]);
gl.uniform4fv(uAccentColor, accentColor ?? [0, 0, 0, 1]);
gl.uniform4fv(uHighlightColor, highlightColor ?? [1, 1, 1, 1]);
gl.uniform1f(uAzimuth, (min(max(azimuth ?? 315, 0), 360) * PI) / 180);
gl.uniform1f(uAltitude, min(max(altitude ?? 45, 0), 90) / 90);
}
else {
this.setFeatureCode(featureCode);
}
// bind vao
gl.bindVertexArray(vao);
// setup the texture
gl.bindTexture(gl.TEXTURE_2D, texture);
// draw elements
gl.drawElements(gl.TRIANGLE_STRIP, count, gl.UNSIGNED_INT, offset);
}
}