s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
307 lines (306 loc) • 12.1 kB
JavaScript
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
import shaderCode from '../shaders/raster.wgsl';
const SHADER_BUFFER_LAYOUT = [
{
// position
arrayStride: 4 * 2,
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
},
];
/** Raster Feature is a standalone raster render storage unit that can be drawn to the GPU */
export class RasterFeature {
layerGuide;
workflow;
tile;
source;
featureCode;
rasterFadeBuffer;
featureCodeBuffer;
fadeStartTime;
parent;
type = 'raster';
bindGroup;
rasterBindGroup;
/**
* @param layerGuide - the layer guide for this feature
* @param workflow - the raster workflow
* @param tile - the tile this feature is drawn on
* @param source - the raster source
* @param featureCode - the encoded feature code that tells the GPU how to compute it's properties
* @param rasterFadeBuffer - 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, rasterFadeBuffer, featureCodeBuffer, fadeStartTime = Date.now(), parent) {
this.layerGuide = layerGuide;
this.workflow = workflow;
this.tile = tile;
this.source = source;
this.featureCode = featureCode;
this.rasterFadeBuffer = rasterFadeBuffer;
this.featureCodeBuffer = featureCodeBuffer;
this.fadeStartTime = fadeStartTime;
this.parent = parent;
this.bindGroup = this.#buildBindGroup();
this.rasterBindGroup = this.#buildRasterBindGroup();
}
/** Draw the feature to the GPU */
draw() {
const { tile, workflow } = this;
workflow.context.setStencilReference(tile.tmpMaskID);
workflow.draw(this);
}
/** Destroy and cleanup the feature */
destroy() {
const { rasterFadeBuffer, featureCodeBuffer } = this;
rasterFadeBuffer.destroy();
featureCodeBuffer.destroy();
}
/**
* Duplicate the raster 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, rasterFadeBuffer, featureCodeBuffer, fadeStartTime, } = this;
const { context } = workflow;
const cE = context.device.createCommandEncoder();
const newRasterFadeBuffer = context.duplicateGPUBuffer(rasterFadeBuffer, cE);
const newFeatureCodeBuffer = context.duplicateGPUBuffer(featureCodeBuffer, cE);
context.device.queue.submit([cE.finish()]);
return new RasterFeature(layerGuide, workflow, tile, source, featureCode, newRasterFadeBuffer, newFeatureCodeBuffer, fadeStartTime, parent);
}
/**
* Build the feature into a bind group
* @returns a new bind group
*/
#buildBindGroup() {
const { workflow, tile, parent, layerGuide, featureCodeBuffer } = this;
const { context } = workflow;
const { mask } = parent ?? tile;
const { layerBuffer, layerCodeBuffer } = layerGuide;
return context.buildGroup('Raster Feature BindGroup', context.featureBindGroupLayout, [
mask.uniformBuffer,
mask.positionBuffer,
layerBuffer,
layerCodeBuffer,
featureCodeBuffer,
]);
}
/**
* Build the raster specific properties into a bind group
* @returns a new raster bind group
*/
#buildRasterBindGroup() {
const { source, workflow, rasterFadeBuffer, layerGuide } = this;
const { context, rasterBindGroupLayout } = workflow;
const { resampling } = layerGuide;
const sampler = context.buildSampler(resampling);
return context.device.createBindGroup({
label: 'Raster BindGroup',
layout: rasterBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: rasterFadeBuffer } },
{ binding: 1, resource: sampler },
{ binding: 2, resource: source.texture.createView() },
],
});
}
}
/** Raster Workflow */
export default class RasterWorkflow {
context;
layerGuides = new Map();
pipeline;
rasterBindGroupLayout;
/** @param context - The WebGPU context */
constructor(context) {
this.context = context;
}
/** Setup the workflow */
async setup() {
this.pipeline = await this.#getPipeline();
}
/** Destroy and cleanup the workflow */
destroy() {
for (const { layerBuffer, layerCodeBuffer } of this.layerGuides.values()) {
layerBuffer.destroy();
layerCodeBuffer.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 } = layerBase;
// PRE) get layer base
const { resampling, fadeDuration } = layer;
let { opacity, saturation, contrast } = layer;
opacity = opacity ?? 1;
saturation = saturation ?? 0;
contrast = contrast ?? 0;
// 1) build definition
const layerDefinition = {
...layerBase,
type: 'raster',
opacity: opacity ?? 1,
saturation: saturation ?? 0,
contrast: contrast ?? 0,
};
// 2) Store layer workflow
const layerCode = [];
for (const paint of [opacity, saturation, contrast]) {
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);
// 4) Store layer guide
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
lch,
fadeDuration: fadeDuration ?? 300,
resampling: resampling ?? 'linear',
layerBuffer,
layerCodeBuffer,
visible,
interactive: false,
opaque: false,
});
return layerDefinition;
}
/**
* Build the source raster data into raster features
* @param rasterData - the input raster data
* @param tile - the tile we are building the features for
*/
buildSource(rasterData, tile) {
const { context } = this;
const { image, size } = rasterData;
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, rasterData, tile);
}
/**
* Build raster features from input raster source
* @param source - the source to build features from
* @param rasterData - the input raster data
* @param tile - the tile we are building the features for
*/
#buildFeatures(source, rasterData, tile) {
const { context } = this;
const { featureGuides } = rasterData;
// 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 rasterFadeBuffer = context.buildGPUBuffer('Raster 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 RasterFeature(layerGuide, this, tile, source, code, rasterFadeBuffer, featureCodeBuffer);
features.push(feature);
}
tile.addFeatures(features);
}
/**
* Build the render pipeline for the raster 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 raster uniforms
this.rasterBindGroupLayout = context.device.createBindGroupLayout({
label: 'Raster BindGroupLayout',
entries: [
// uniforms
{ 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' } },
],
});
const module = device.createShaderModule({ code: shaderCode });
const layout = device.createPipelineLayout({
bindGroupLayouts: [frameBindGroupLayout, featureBindGroupLayout, this.rasterBindGroupLayout],
});
const stencilState = {
compare: 'equal',
failOp: 'keep',
depthFailOp: 'keep',
passOp: 'replace',
};
return await device.createRenderPipelineAsync({
label: 'Raster Pipeline',
layout,
vertex: { module, entryPoint: 'vMain', buffers: SHADER_BUFFER_LAYOUT },
fragment: {
module,
entryPoint: 'fMain',
targets: [{ format, blend: defaultBlend }],
},
primitive: {
topology: 'triangle-strip',
cullMode: projection === 'S2' ? 'back' : 'front',
stripIndexFormat: 'uint32',
},
multisample: { count: sampleCount },
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: stencilState,
stencilBack: stencilState,
stencilReadMask: 0xffffffff,
stencilWriteMask: 0xffffffff,
},
});
}
/**
* Draw a raster feature to the GPU
* @param feature - raster feature guide
*/
draw(feature) {
const { layerGuide: { visible }, bindGroup, rasterBindGroup, 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, rasterBindGroup);
// draw
passEncoder.drawIndexed(count, 1, offset, 0, 0);
}
}