s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
130 lines (129 loc) • 4.81 kB
JavaScript
import { Color } from 'style/color/index.js';
import shaderCode from '../shaders/wallpaper.wgsl';
/** Wallpaper Workflow renders a user styled wallpaper to the GPU */
export default class WallpaperWorkflow {
context;
scheme;
tileSize = 512;
scale = new Float32Array([0, 0]);
pipeline;
#uniformBuffer;
#wallpaperBindGroupLayout;
#bindGroup;
/** @param context - The WebGPU context */
constructor(context) {
this.context = context;
// setup scheme
this.scheme = {
background: new Color('#000'),
fade1: new Color('#000'),
fade2: new Color('#000'),
halo: new Color('#000'),
};
}
/** Setup the wallpaper workflow */
async setup() {
const { context } = this;
// prep the matrix buffer
this.#uniformBuffer = context.buildGPUBuffer('Wallpaper Uniform Buffer', new Float32Array(20), GPUBufferUsage.UNIFORM);
this.pipeline = await this.#getPipeline();
}
/** Cleanup the wallpaper workflow */
destroy() {
this.#uniformBuffer.destroy();
}
/**
* Update the wallpaper style
* @param style - input user defined style
*/
updateStyle(style) {
const { scheme, context } = this;
const { background, fade1, fade2, halo } = style.wallpaper ?? {};
// inject wallpaper into scheme
if (background !== undefined)
scheme.background = new Color(background);
if (fade1 !== undefined)
scheme.fade1 = new Color(fade1);
if (fade2 !== undefined)
scheme.fade2 = new Color(fade2);
if (halo !== undefined)
scheme.halo = new Color(halo);
// inject uniforms
this.#updateUniforms();
// build the bind group
this.#bindGroup = context.buildGroup('Wallpaper', this.#wallpaperBindGroupLayout, [
this.#uniformBuffer,
]);
}
/**
* Update the scale
* @param projector - Projector
*/
#updateScale(projector) {
const { min, pow } = Math;
const { dirty, zoom, aspect, multiplier } = projector;
if (!dirty)
return;
const radius = this.tileSize * min(pow(2, zoom), 32_768);
const mult2 = multiplier / 2;
this.scale[0] = radius / (aspect.x / mult2);
this.scale[1] = radius / (aspect.y / mult2);
this.context.device.queue.writeBuffer(this.#uniformBuffer, 16 * 4, this.scale);
}
/** Update uniforms. Only updates on style change */
#updateUniforms() {
const { context, scheme } = this;
context.device.queue.writeBuffer(this.#uniformBuffer, 0, new Float32Array([
...scheme.fade1.getRGB(true),
...scheme.fade2.getRGB(true),
...scheme.halo.getRGB(true),
...scheme.background.getRGB(true),
]));
}
/**
* Setup the GPU Pipeline for drawing.
* 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 Pipeline
*/
async #getPipeline() {
const { device, format, sampleCount, frameBindGroupLayout } = this.context;
// prep Wallpaper uniforms
this.#wallpaperBindGroupLayout = this.context.buildLayout('Wallpaper', ['uniform'], GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
const module = device.createShaderModule({ code: shaderCode });
const layout = device.createPipelineLayout({
bindGroupLayouts: [frameBindGroupLayout, this.#wallpaperBindGroupLayout],
});
return await device.createRenderPipelineAsync({
label: 'Wallpaper Pipeline',
layout,
vertex: { module, entryPoint: 'vMain' },
fragment: { module, entryPoint: 'fMain', targets: [{ format }] },
primitive: { topology: 'triangle-list', cullMode: 'none' },
multisample: { count: sampleCount },
depthStencil: {
depthWriteEnabled: false,
depthCompare: 'always',
format: 'depth24plus-stencil8',
},
});
}
/**
* Draw the wallpaper to the GPU
* @param projector - Projector
*/
draw(projector) {
const { context } = this;
// get current source data
const { passEncoder } = context;
// update scale if needed
if (projector.dirty)
this.#updateScale(projector);
// setup pipeline, bind groups, & buffers
context.setRenderPipeline(this.pipeline);
passEncoder.setBindGroup(1, this.#bindGroup);
// draw the quad
passEncoder.draw(6);
}
}