s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
340 lines (339 loc) • 13.6 kB
JavaScript
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
import shaderCode from '../shaders/hillshade.wgsl';
const SHADER_BUFFER_LAYOUT = [
{
// position
arrayStride: 4 * 2,
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
},
];
/** Hillshade Feature is a standalone hillshade render storage unit that can be drawn to the GPU */
export class HilllshadeFeature {
layerGuide;
workflow;
tile;
source;
featureCode;
hillshadeFadeBuffer;
featureCodeBuffer;
fadeStartTime;
parent;
type = 'hillshade';
sourceName;
fadeDuration;
bindGroup;
hillshadeBindGroup;
/**
* @param layerGuide - the layer guide for this feature
* @param workflow - the hillshade workflow
* @param tile - the tile this feature is drawn on
* @param source - the hillshade source
* @param featureCode - the encoded feature code
* @param hillshadeFadeBuffer - the fade buffer
* @param featureCodeBuffer - the feature code buffer
* @param fadeStartTime - the start time of the fade for smooth transitions
* @param parent - the parent tile if applicable
*/
constructor(layerGuide, workflow, tile, source, featureCode, hillshadeFadeBuffer, featureCodeBuffer, fadeStartTime = Date.now(), parent) {
this.layerGuide = layerGuide;
this.workflow = workflow;
this.tile = tile;
this.source = source;
this.featureCode = featureCode;
this.hillshadeFadeBuffer = hillshadeFadeBuffer;
this.featureCodeBuffer = featureCodeBuffer;
this.fadeStartTime = fadeStartTime;
this.parent = parent;
const { sourceName, fadeDuration } = layerGuide;
this.sourceName = sourceName;
this.fadeDuration = fadeDuration;
this.bindGroup = this.#buildBindGroup();
this.hillshadeBindGroup = this.#buildHillshadeBindGroup();
}
/** Draw the feature to the GPU */
draw() {
const { tile, workflow } = this;
workflow.context.setStencilReference(tile.tmpMaskID);
workflow.draw(this);
}
/** Destroy the feature */
destroy() {
const { hillshadeFadeBuffer, featureCodeBuffer } = this;
hillshadeFadeBuffer.destroy();
featureCodeBuffer.destroy();
}
/**
* Duplicate this feature
* @param tile - the tile this feature is drawn on
* @param parent - the parent tile if applicable
* @returns the duplicated feature
*/
duplicate(tile, parent) {
const { layerGuide, workflow, source, featureCode, hillshadeFadeBuffer, featureCodeBuffer, fadeStartTime, } = this;
const { context } = workflow;
const cE = context.device.createCommandEncoder();
const newHillshadeFadeBuffer = context.duplicateGPUBuffer(hillshadeFadeBuffer, cE);
const newFeatureCodeBuffer = context.duplicateGPUBuffer(featureCodeBuffer, cE);
context.device.queue.submit([cE.finish()]);
return new HilllshadeFeature(layerGuide, workflow, tile, source, featureCode, newHillshadeFadeBuffer, newFeatureCodeBuffer, fadeStartTime, parent);
}
/**
* Build the bind group for the hillshade feature
* @returns the GPU Bind Group for the hillshade feature
*/
#buildBindGroup() {
const { workflow, tile, parent, layerGuide, featureCodeBuffer } = this;
const { context } = workflow;
const { mask } = parent ?? tile;
const { layerBuffer, layerCodeBuffer } = layerGuide;
return context.buildGroup('Hillshade Feature BindGroup', context.featureBindGroupLayout, [
mask.uniformBuffer,
mask.positionBuffer,
layerBuffer,
layerCodeBuffer,
featureCodeBuffer,
]);
}
/**
* Build the bind group for the hillshade feature
* @returns the GPU Bind Group for the hillshade feature
*/
#buildHillshadeBindGroup() {
const { source, workflow, hillshadeFadeBuffer, layerGuide } = this;
const { context, hillshadeBindGroupLayout } = workflow;
const { unpackBuffer } = layerGuide;
return context.device.createBindGroup({
label: 'Hillshade BindGroup',
layout: hillshadeBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: hillshadeFadeBuffer } },
{ binding: 1, resource: context.defaultSampler },
{ binding: 2, resource: source.texture.createView() },
{ binding: 3, resource: { buffer: unpackBuffer } },
],
});
}
}
/** Hillshade Workflow */
export default class HillshadeWorkflow {
context;
layerGuides = new Map();
pipeline;
hillshadeBindGroupLayout;
/** @param context - The WebGPU context */
constructor(context) {
this.context = context;
}
/** Setup the workflow */
async setup() {
// create pipelines
this.pipeline = await this.#getPipeline();
}
/** Destroy and cleanup the workflow */
destroy() {
for (const { layerBuffer, layerCodeBuffer, unpackBuffer } of this.layerGuides.values()) {
layerBuffer.destroy();
layerCodeBuffer.destroy();
unpackBuffer.destroy();
}
}
/**
* Build the layer definition for this workflow
* @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 { context } = 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',
// paint
shadowColor,
accentColor,
highlightColor,
azimuth,
altitude,
opacity,
// layout
unpack,
};
// 2) Store layer workflow
const layerCode = [];
for (const paint of [opacity, shadowColor, accentColor, highlightColor, azimuth, altitude]) {
layerCode.push(...encodeLayerAttribute(paint, lch));
}
// 3) Setup layer buffers in GPU
const layerBuffer = context.buildGPUBuffer('Layer Uniform Buffer', new Float32Array([context.getDepthPosition(layerIndex), ~~lch]), GPUBufferUsage.UNIFORM);
const layerCodeBuffer = context.buildGPUBuffer('Layer Code Buffer', new Float32Array(layerCode), GPUBufferUsage.STORAGE);
const unpackData = [
unpack.offset,
unpack.zFactor,
unpack.rMultiplier,
unpack.gMultiplier,
unpack.bMultiplier,
unpack.aMultiplier,
];
const unpackBuffer = context.buildGPUBuffer('Unpack Buffer', new Float32Array(unpackData), GPUBufferUsage.UNIFORM);
// 4) Store layer guide
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
lch,
fadeDuration: fadeDuration ?? 300,
layerBuffer,
layerCodeBuffer,
unpackBuffer,
visible,
interactive: interactive ?? false,
opaque: false,
});
return layerDefinition;
}
/**
* Build the source hillshade data into hillshade features
* @param hillshadeData - the input hillshade data
* @param tile - the tile we are building the features for
*/
buildSource(hillshadeData, tile) {
const { context } = this;
const { image, size } = hillshadeData;
const { mask } = tile;
const texture = context.buildTexture(image, size);
// prep buffers
const source = {
type: 'raster',
texture,
vertexBuffer: mask.vertexBuffer,
indexBuffer: mask.indexBuffer,
count: mask.count,
offset: mask.offset,
/** Destroy the raster source */
destroy: () => {
texture.destroy();
},
};
// build features
this.#buildFeatures(source, hillshadeData, tile);
}
/**
* Build hillshade features from input hillshade source
* @param source - the source to build features from
* @param hillshadeData - the input hillshade data
* @param tile - the tile we are building the features for
*/
#buildFeatures(source, hillshadeData, tile) {
const { context } = this;
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 hillshadeFadeBuffer = context.buildGPUBuffer('Hillshade Uniform Buffer', new Float32Array([1]), GPUBufferUsage.UNIFORM);
const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array(code.length > 0 ? code : [0]), GPUBufferUsage.STORAGE);
const feature = new HilllshadeFeature(layerGuide, this, tile, source, code, hillshadeFadeBuffer, featureCodeBuffer);
features.push(feature);
}
tile.addFeatures(features);
}
/**
* Build the render pipeline for the hillshade workflow
* https://programmer.ink/think/several-best-practices-of-webgpu.html
* BEST PRACTICE 6: it is recommended to create pipeline asynchronously
* BEST PRACTICE 7: explicitly define pipeline layouts
* @returns the render pipeline
*/
async #getPipeline() {
const { context } = this;
const { device, format, defaultBlend, projection, sampleCount, frameBindGroupLayout, featureBindGroupLayout, } = context;
// prep hillshade uniforms
this.hillshadeBindGroupLayout = context.device.createBindGroupLayout({
label: 'Hillshade BindGroupLayout',
entries: [
// uniform
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
// sampler
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
// texture
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
// unpack
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
],
});
const module = device.createShaderModule({ code: shaderCode });
const layout = device.createPipelineLayout({
bindGroupLayouts: [
frameBindGroupLayout,
featureBindGroupLayout,
this.hillshadeBindGroupLayout,
],
});
const cullMode = projection === 'S2' ? 'back' : 'front';
const stencilState = {
compare: 'equal',
failOp: 'keep',
depthFailOp: 'keep',
passOp: 'replace',
};
return await device.createRenderPipelineAsync({
label: 'Hillshade Pipeline',
layout,
vertex: { module, entryPoint: 'vMain', buffers: SHADER_BUFFER_LAYOUT },
fragment: { module, entryPoint: 'fMain', targets: [{ format, blend: defaultBlend }] },
primitive: { topology: 'triangle-strip', cullMode, stripIndexFormat: 'uint32' },
multisample: { count: sampleCount },
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: stencilState,
stencilBack: stencilState,
stencilReadMask: 0xffffffff,
stencilWriteMask: 0xffffffff,
},
});
}
/**
* Draw a screen quad with the hillshade feature's properties
* @param feature - hillshade feature
*/
draw(feature) {
const { layerGuide: { visible }, bindGroup, hillshadeBindGroup, source, } = feature;
if (!visible)
return;
// get current source data
const { passEncoder } = this.context;
const { vertexBuffer, indexBuffer, count, offset } = source;
// setup pipeline, bind groups, & buffers
this.context.setRenderPipeline(this.pipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.setIndexBuffer(indexBuffer, 'uint32');
passEncoder.setBindGroup(1, bindGroup);
passEncoder.setBindGroup(2, hillshadeBindGroup);
// draw
passEncoder.drawIndexed(count, 1, offset, 0, 0);
}
}