UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

382 lines (381 loc) 16.1 kB
import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import shaderCode from '../shaders/point.wgsl'; const SHADER_BUFFER_LAYOUT = [ { // pos arrayStride: 4 * 2, stepMode: 'instance', attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }], }, ]; /** Point Feature is a standalone point render storage unit that can be drawn to the GPU */ export class PointFeature { workflow; source; layerGuide; tile; count; offset; featureCode; pointBoundsBuffer; pointInteractiveBuffer; featureCodeBuffer; parent; type = 'point'; bindGroup; pointBindGroup; pointInteractiveBindGroup; /** * @param workflow - the point workflow * @param source - the point source * @param layerGuide - layer guide for this feature * @param tile - the tile this feature is drawn on * @param count - the number of points * @param offset - the offset of the points * @param featureCode - the encoded feature code * @param pointBoundsBuffer - the bounds of the points * @param pointInteractiveBuffer - the interactive buffer * @param featureCodeBuffer - the encoded feature code that tells the GPU how to compute it's properties * @param parent - the parent tile if applicable */ constructor(workflow, source, layerGuide, tile, count, offset, featureCode, pointBoundsBuffer, pointInteractiveBuffer, featureCodeBuffer, parent) { this.workflow = workflow; this.source = source; this.layerGuide = layerGuide; this.tile = tile; this.count = count; this.offset = offset; this.featureCode = featureCode; this.pointBoundsBuffer = pointBoundsBuffer; this.pointInteractiveBuffer = pointInteractiveBuffer; this.featureCodeBuffer = featureCodeBuffer; this.parent = parent; this.bindGroup = this.#buildBindGroup(); this.pointBindGroup = this.#buildPointBindGroup(); this.pointInteractiveBindGroup = this.#buildPointInteractiveBindGroup(); } /** Draw the feature to the GPU */ draw() { const { tile, workflow } = this; workflow.context.setStencilReference(tile.tmpMaskID); workflow.draw(this); } /** Compute the feature's interactivity with the mouse */ compute() { this.workflow.computeInteractive(this); } /** Destroy and cleanup the feature */ destroy() { const { pointBoundsBuffer, pointInteractiveBuffer, featureCodeBuffer } = this; pointBoundsBuffer.destroy(); pointInteractiveBuffer.destroy(); featureCodeBuffer.destroy(); } /** * Duplicate this feature * @param tile - the tile this feature is drawn on * @param parent - the parent tile if applicable * @param bounds - the bounds of the tile if applicable * @returns the duplicated feature */ duplicate(tile, parent, bounds) { const { workflow, source, layerGuide, count, offset, featureCode, pointBoundsBuffer, pointInteractiveBuffer, featureCodeBuffer, } = this; const { context } = workflow; const cE = context.device.createCommandEncoder(); const newFeatureCodeBuffer = context.duplicateGPUBuffer(featureCodeBuffer, cE); const newPointBoundsBuffer = bounds !== undefined ? context.buildGPUBuffer('Point Uniform Buffer', new Float32Array(bounds), GPUBufferUsage.UNIFORM) : context.duplicateGPUBuffer(pointBoundsBuffer, cE); const newPointInteractiveBuffer = context.duplicateGPUBuffer(pointInteractiveBuffer, cE); context.device.queue.submit([cE.finish()]); return new PointFeature(workflow, source, layerGuide, tile, count, offset, featureCode, newPointBoundsBuffer, newPointInteractiveBuffer, newFeatureCodeBuffer, parent); } /** * Build the bind group for the point feature * @returns the GPU Bind Group for the point feature */ #buildBindGroup() { const { workflow, tile, parent, layerGuide, featureCodeBuffer } = this; const { context } = workflow; const { mask } = parent ?? tile; const { layerBuffer, layerCodeBuffer } = layerGuide; return context.buildGroup('Point Feature BindGroup', context.featureBindGroupLayout, [ mask.uniformBuffer, mask.positionBuffer, layerBuffer, layerCodeBuffer, featureCodeBuffer, ]); } /** * Build a bind group with point specific properties * @returns the GPU Bind Group for the point */ #buildPointBindGroup() { const { workflow, pointBoundsBuffer } = this; const { context, pointBindGroupLayout } = workflow; return context.buildGroup('Point BindGroup', pointBindGroupLayout, [pointBoundsBuffer]); } /** * Build a bind group with point interactive specific properties * @returns the GPU Bind Group for the point */ #buildPointInteractiveBindGroup() { const { workflow, pointBoundsBuffer, pointInteractiveBuffer, source } = this; const { context, pointInteractiveBindGroupLayout } = workflow; return context.buildGroup('Point Interactive BindGroup', pointInteractiveBindGroupLayout, [ pointBoundsBuffer, pointInteractiveBuffer, source.vertexBuffer, source.idBuffer, ]); } } /** Point Workflow */ export default class PointWorkflow { context; layerGuides = new Map(); pipeline; interactivePipeline; pointInteractiveBindGroupLayout; pointBindGroupLayout; module; /** @param context - The WebGPU context */ constructor(context) { this.context = context; } /** Setup the workflow */ async setup() { this.module = this.context.device.createShaderModule({ code: shaderCode }); this.pipeline = await this.#getPipeline(); this.interactivePipeline = await this.#getComputePipeline(); } /** 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 let { radius, opacity, color, stroke, strokeWidth, interactive, cursor, geoFilter } = layer; radius = radius ?? 1; opacity = opacity ?? 1; color = color ?? 'rgba(0, 0, 0, 0)'; stroke = stroke ?? 'rgba(0, 0, 0, 0)'; strokeWidth = strokeWidth ?? 1; interactive = interactive ?? false; cursor = cursor ?? 'default'; geoFilter = geoFilter ?? ['line', 'poly']; // 1) build definition const layerDefinition = { ...layerBase, type: 'point', // paint radius, opacity, color, stroke, strokeWidth, // propreties geoFilter, interactive, cursor, }; // 2) build the layerCode const layerCode = []; for (const paint of [radius, strokeWidth, opacity, color, stroke]) { 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, layerBuffer, layerCodeBuffer, lch, interactive, cursor, visible, opaque: false, }); return layerDefinition; } /** * Build the source point data into point features * @param pointData - the input point data * @param tile - the tile we are building the features for */ buildSource(pointData, tile) { const { context } = this; const { vertexBuffer, idBuffer, featureGuideBuffer } = pointData; // prep buffers const source = { type: 'point', vertexBuffer: context.buildGPUBuffer('Point Vertex Buffer', new Float32Array(vertexBuffer), GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE), idBuffer: context.buildGPUBuffer('Point Index Buffer', new Uint32Array(idBuffer), GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE), /** destroy the point source */ destroy: () => { const { vertexBuffer, idBuffer } = source; vertexBuffer.destroy(); idBuffer.destroy(); }, }; // build features this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer)); } /** * Build point features from input point source * @param source - the input point source * @param tile - the tile we are building the features for * @param featureGuideArray - the feature guide to help build the features properties */ #buildFeatures(source, tile, featureGuideArray) { const { context } = this; const features = []; const lgl = featureGuideArray.length; let i = 0; while (i < lgl) { // grab the size, layerIndex, count, and offset, and update the index const [layerIndex, count, offset, encodingSize] = featureGuideArray.slice(i, i + 4); i += 4; // build featureCode let featureCode = [0]; featureCode = encodingSize > 0 ? [...featureGuideArray.slice(i, i + encodingSize)] : [0]; // update index i += encodingSize; const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; const pointBoundsBuffer = context.buildGPUBuffer('Point Uniform Buffer', new Float32Array([0, 0, 1, 1]), GPUBufferUsage.UNIFORM); const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array(featureCode), GPUBufferUsage.STORAGE); const pointInteractiveBuffer = context.buildGPUBuffer('Point Interactive Buffer', new Uint32Array([offset, count]), GPUBufferUsage.UNIFORM); const feature = new PointFeature(this, source, layerGuide, tile, count, offset, featureCode, pointBoundsBuffer, pointInteractiveBuffer, featureCodeBuffer); features.push(feature); } tile.addFeatures(features); } /** * Build the render pipeline for the point 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 { module, context } = this; const { device, format, defaultBlend, sampleCount, frameBindGroupLayout, featureBindGroupLayout, } = context; this.pointBindGroupLayout = context.buildLayout('Point', ['uniform'], GPUShaderStage.VERTEX); const layout = device.createPipelineLayout({ bindGroupLayouts: [frameBindGroupLayout, featureBindGroupLayout, this.pointBindGroupLayout], }); const stencilState = { compare: 'always', failOp: 'keep', depthFailOp: 'keep', passOp: 'replace', }; return await device.createRenderPipelineAsync({ label: 'Point Pipeline', layout, vertex: { module, entryPoint: 'vMain', buffers: SHADER_BUFFER_LAYOUT }, fragment: { module, entryPoint: 'fMain', targets: [{ format, blend: defaultBlend }], }, primitive: { topology: 'triangle-list', cullMode: 'none' }, multisample: { count: sampleCount }, depthStencil: { depthWriteEnabled: true, depthCompare: 'less-equal', format: 'depth24plus-stencil8', stencilFront: stencilState, stencilBack: stencilState, stencilReadMask: 0xffffffff, stencilWriteMask: 0xffffffff, }, }); } /** * Build a compute pipeline to check for interactive point data that interects with the mouse * 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 GPU compute pipeline */ async #getComputePipeline() { const { context, module } = this; const { device, frameBindGroupLayout, featureBindGroupLayout, interactiveBindGroupLayout } = context; this.pointInteractiveBindGroupLayout = device.createBindGroupLayout({ label: 'Point Interactive BindGroupLayout', entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // bounds { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // interactive offset & count { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // positions { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // ids ], }); const layout = device.createPipelineLayout({ bindGroupLayouts: [ frameBindGroupLayout, featureBindGroupLayout, this.pointInteractiveBindGroupLayout, interactiveBindGroupLayout, ], }); return await context.device.createComputePipelineAsync({ label: 'Point Interactive Compute Pipeline', layout, compute: { module, entryPoint: 'interactive' }, }); } /** * Draw a point feature to the GPU * @param feature - point feature guide */ draw(feature) { const { layerGuide: { visible }, bindGroup, pointBindGroup, source, count, offset, } = feature; if (!visible) return; // get current source data const { passEncoder } = this.context; const { vertexBuffer } = source; // setup pipeline, bind groups, & buffers this.context.setRenderPipeline(this.pipeline); passEncoder.setBindGroup(1, bindGroup); passEncoder.setBindGroup(2, pointBindGroup); passEncoder.setVertexBuffer(0, vertexBuffer); // draw passEncoder.draw(6, count, 0, offset); } /** * Compute the interactive features that interact with the mouse * @param feature - point feature guide */ computeInteractive(feature) { const { layerGuide: { visible }, bindGroup, pointInteractiveBindGroup, count, } = feature; if (!visible) return; const { interactiveBindGroup, computePass } = this.context; this.context.setComputePipeline(this.interactivePipeline); // set bind group & draw computePass.setBindGroup(1, bindGroup); computePass.setBindGroup(2, pointInteractiveBindGroup); computePass.setBindGroup(3, interactiveBindGroup); computePass.dispatchWorkgroups(Math.ceil(count / 64)); } }