UNPKG

s2maps-gpu

Version:

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

216 lines (215 loc) 7.92 kB
import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import shaderCode from '../shaders/shade.wgsl'; const SHADER_BUFFER_LAYOUT = [ { // position arrayStride: 4 * 2, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }], }, ]; /** Shade Feature is a standalone shade render storage unit that can be drawn to the GPU */ export class ShadeFeature { workflow; tile; layerIndex; layerGuide; featureCodeBuffer; type = 'shade'; maskLayer = true; source; count; offset; featureCode = [0]; bindGroup; /** * @param workflow - the shade workflow * @param tile - the tile that the feature is drawn on * @param layerIndex - the layer's index * @param layerGuide - the layer guide for this feature * @param featureCodeBuffer - the encoded feature code that tells the GPU how to compute it's properties */ constructor(workflow, tile, layerIndex, layerGuide, featureCodeBuffer) { this.workflow = workflow; this.tile = tile; this.layerIndex = layerIndex; this.layerGuide = layerGuide; this.featureCodeBuffer = featureCodeBuffer; const { mask } = tile; this.source = mask; this.count = mask.count; this.offset = mask.offset; this.bindGroup = this.#buildBindGroup(); } /** Draw the feature to the GPU */ draw() { const { workflow } = this; workflow.context.setStencilReference(this.tile.tmpMaskID); workflow.draw(this); } /** Delete and cleanup the feature */ destroy() { this.featureCodeBuffer.destroy(); } /** * Build the bind group for the feature * @returns the feature's GPU bind group */ #buildBindGroup() { const { workflow, tile, layerGuide, featureCodeBuffer } = this; const { context } = workflow; const { mask } = tile; const { layerBuffer, layerCodeBuffer } = layerGuide; return context.buildGroup('Shade Feature BindGroup', context.featureBindGroupLayout, [ mask.uniformBuffer, mask.positionBuffer, layerBuffer, layerCodeBuffer, featureCodeBuffer, ]); } } /** Shade Workflow */ export default class ShadeWorkflow { context; layerGuide; pipeline; /** @param context - The WebGPU context */ constructor(context) { this.context = context; } /** Setup the shade workflow */ async setup() { this.pipeline = await this.#getPipeline(); } /** Cleanup the shade workflow */ destroy() { const { layerGuide } = this; if (layerGuide === undefined) return; const { layerBuffer, layerCodeBuffer } = layerGuide; 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 { lch, layerIndex } = layerBase; let { color } = layer; color = color ?? 'rgb(0.6, 0.6, 0.6)'; // 2) build the layerCode const layerCode = []; layerCode.push(...encodeLayerAttribute(color, 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 the layerDefinition and return const definition = { ...layerBase, type: 'shade', // layout color, }; // 5) store the layerGuide this.layerGuide = { ...definition, sourceName: 'mask', layerCode, layerBuffer, layerCodeBuffer, interactive: false, opaque: false, }; return definition; } /** * Build a mask feature for the tile that helps the shade guide work * @param shadeGuide - the shade guide * @param tile - the tile that needs a mask */ buildMaskFeature(shadeGuide, tile) { const { layerIndex, minzoom, maxzoom } = shadeGuide; const { context, layerGuide } = this; const { zoom } = tile; // not in the zoom range, ignore if (layerGuide === undefined || zoom < minzoom || zoom > maxzoom) return; const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array([0]), GPUBufferUsage.STORAGE); const feature = new ShadeFeature(this, tile, layerIndex, layerGuide, featureCodeBuffer); tile.addFeatures([feature]); } /** * Build the render pipeline for the shade 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 { device, format, sampleCount, frameBindGroupLayout, featureBindGroupLayout } = this.context; const module = device.createShaderModule({ code: shaderCode }); const layout = device.createPipelineLayout({ bindGroupLayouts: [frameBindGroupLayout, featureBindGroupLayout], }); const stencilState = { compare: 'always', failOp: 'keep', depthFailOp: 'keep', passOp: 'keep', }; return await device.createRenderPipelineAsync({ label: 'Shade Pipeline', layout, vertex: { module, entryPoint: 'vMain', buffers: SHADER_BUFFER_LAYOUT }, fragment: { module, entryPoint: 'fMain', targets: [ { format, blend: { // operation: common operation color: { srcFactor: 'dst', dstFactor: 'zero', operation: 'add' }, // operation: assuming you want the same for alpha alpha: { srcFactor: 'dst', dstFactor: 'zero', operation: 'add' }, }, }, ], }, primitive: { topology: 'triangle-strip', cullMode: 'back', stripIndexFormat: 'uint32' }, multisample: { count: sampleCount }, depthStencil: { depthWriteEnabled: true, depthCompare: 'less', format: 'depth24plus-stencil8', stencilFront: stencilState, stencilBack: stencilState, stencilReadMask: 0, stencilWriteMask: 0, }, }); } /** * Draw a shade feature to the GPU * @param feature - shade feature guide */ draw(feature) { const { layerGuide: { visible }, source, bindGroup, } = feature; if (!visible) return; const { context, pipeline } = this; const { passEncoder } = context; const { vertexBuffer, indexBuffer, count, offset } = source; // setup pipeline, bind groups, & buffers this.context.setRenderPipeline(pipeline); passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.setIndexBuffer(indexBuffer, 'uint32'); passEncoder.setBindGroup(1, bindGroup); // draw passEncoder.drawIndexed(count, 1, offset); } }