s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
205 lines (204 loc) • 7.78 kB
JavaScript
import { Color } from 'style/color/index.js';
import { adjustURL } from 'util/index.js';
import { degToRad } from 'gis-tools/index.js';
import shaderCode from '../shaders/skybox.wgsl';
import { invert, multiply, perspective, rotate } from 'ui/camera/projector/mat4.js';
/** Skybox Workflow renders a user styled skybox to the GPU */
export default class SkyboxWorkflow {
context;
facesReady = 0;
ready = false;
fov = degToRad(80);
angle = degToRad(40);
matrix = new Float32Array(16);
pipeline;
#matrixBuffer;
#cubeMap;
#sampler;
#skyboxBindGroupLayout;
#bindGroup;
/** @param context - The WebGPU context */
constructor(context) {
this.context = context;
}
/** Setup the skybox workflow */
async setup() {
const { context } = this;
const { device } = context;
// prep the matrix buffer
this.#matrixBuffer = context.buildGPUBuffer('Skybox Uniform Buffer', this.matrix, GPUBufferUsage.UNIFORM);
// prep the sampler
this.#sampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge',
addressModeW: 'clamp-to-edge',
});
this.pipeline = await this.#getPipeline();
}
/** Destroy the skybox workflow */
destroy() {
this.#matrixBuffer.destroy();
this.#cubeMap.destroy();
}
/**
* Update the skybox style
* @param style - user defined style
* @param camera - The camera
* @param urlMap - The url map to properly resolve urls
*/
updateStyle(style, camera, urlMap) {
const { context } = this;
const { device } = context;
const { skybox } = style;
const { type, size, loadingBackground } = style.skybox ?? {};
let path = skybox?.path;
if (typeof path !== 'string')
throw new Error('Skybox path must be a string');
if (typeof type !== 'string')
throw new Error('Skybox type must be a string');
if (typeof size !== 'number')
throw new Error('Skybox size must be a number');
path = adjustURL(path, urlMap);
// grab clear color and set inside painter
if (loadingBackground !== undefined) {
context.setClearColor(new Color(loadingBackground ?? 'rgb(0, 0, 0)').getRGB());
}
// build a cube map and sampler
if (this.#cubeMap !== undefined)
this.#cubeMap.destroy();
this.#cubeMap = context.buildTexture(null, size, size, 6);
// build the bind group
this.#bindGroup = device.createBindGroup({
label: 'Skybox BindGroup',
layout: this.#skyboxBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.#matrixBuffer } },
{ binding: 1, resource: this.#sampler },
{ binding: 2, resource: this.#cubeMap.createView({ dimension: 'cube' }) },
],
});
// reset our tracking variables
this.facesReady = 0;
this.ready = false;
// request each face and assign to cube map
for (let i = 0; i < 6; i++)
void this.#getImage(i, `${path}/${size}/${i}.${type}`, camera);
}
/**
* Setup the skybox pipeline
* 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 WebGPU pipeline
*/
async #getPipeline() {
const { device, format, sampleCount, frameBindGroupLayout } = this.context;
// prep skybox uniforms
this.#skyboxBindGroupLayout = device.createBindGroupLayout({
label: 'Skybox BindGroupLayout',
entries: [
// matrix
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
// sampler
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
// texture
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float', viewDimension: 'cube' },
},
],
});
const module = device.createShaderModule({ label: 'Skybox Shader Module', code: shaderCode });
const layout = device.createPipelineLayout({
label: 'Skybox Pipeline Layout',
bindGroupLayouts: [frameBindGroupLayout, this.#skyboxBindGroupLayout],
});
return await device.createRenderPipelineAsync({
label: 'Skybox 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',
},
});
}
/**
* Get the appropriate cube map image and upload the data to the GPU
* @param index - the index
* @param path - the path to the image
* @param camera - the camera
*/
async #getImage(index, path, camera) {
const { context } = this;
const data = await fetch(path)
.then(async (res) => {
if (res.status !== 200 && res.status !== 206)
return;
return await res.blob();
})
.catch(() => {
return undefined;
});
if (data === undefined)
return;
const image = await createImageBitmap(data);
// upload to texture
context.uploadTextureData(this.#cubeMap, image, image.width, image.height, undefined, {
x: 0,
y: 0,
z: index,
});
// set the projector as dirty to ensure a proper initial render
camera.projector.reset();
// call the full re-render
camera.render();
// update the ready count
this.facesReady++;
// if all faces are uploaded, set the skybox as ready
if (this.facesReady === 6)
this.ready = true;
}
/**
* Update to the projector's current matrix
* @param projector - The projector
*/
#updateMatrix(projector) {
const { context, fov, angle, matrix } = this;
const { aspect, lon, lat } = projector;
// create a perspective matrix
perspective(matrix, fov, aspect.x / aspect.y, 1, 10000);
// rotate perspective
rotate(matrix, [degToRad(lat), degToRad(lon), angle]);
// this is a simplified "lookat", since we maintain a set camera position
multiply(matrix, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
// invert view
invert(matrix);
// update matrix for the GPU
context.device.queue.writeBuffer(this.#matrixBuffer, 0, matrix);
}
/**
* Draw the skybox
* @param projector - The projector
*/
draw(projector) {
// get current source data
const { passEncoder } = this.context;
// update matrix if necessary
if (projector.dirty)
this.#updateMatrix(projector);
// setup pipeline, bind groups, & buffers
this.context.setRenderPipeline(this.pipeline);
passEncoder.setBindGroup(1, this.#bindGroup);
// draw the quad
passEncoder.draw(6);
}
}