LineRenderer = {
const vertexShader = `
@group(0) @binding(0) var<uniform> matrix: mat4x4f;
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) color: vec4f
}
@vertex
fn vs(
@location(0) position: vec4f,
@location(1) color: vec4f
) -> VertexOut {
var output: VertexOut;
// FIXME should pass width & height as uniforms instead of hardcoding
output.position = matrix * vec4f((position.xy / ${TILE_WIDTH / 2} - 1.0), position.z, 1.0);
output.color = color;
return output;
}
`;
const fragmentShader = `
@fragment
fn fs(@location(0) color: vec4f) -> @location(0) vec4f {
return color;
}
`;
class LineRenderer {
device ;
context ;
canvas ;
vertexBuffer ;
colorBuffer ;
uniformBuffer /*: GPUBuffer */;
pipeline /*: GPURenderPipeline */;
options /*: { randomSegmentColors?: boolean } */ = {};
multisampleTexture;
renderPassDescriptor = {
colorAttachments: [
{
view: null,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
}
constructor(device /*: GPUDevice */, context /*: CanvasRenderingContext2D */, lines, options = {}) {
this.device = device;
this.canvas = context.canvas;
this.context = context;
this.options = options;
const vertexData /*: number[] */ = [];
const colorData /*: number[] */ = [];
for (const line of lines) {
const { vertices, color } = line;
for (let i = 0; i < vertices.length - 1; i++) {
// FIXME performance for continuous lines
vertexData.push(
// Start
vertices[i][0],
vertices[i][1],
vertices[i][2],
1,
// End
vertices[i + 1][0],
vertices[i + 1][1],
vertices[i + 1][2],
1,
);
if (!color || this.options?.randomSegmentColors) {
const c = [util.rand(), util.rand(), util.rand(), 1]
// prettier-ignore
colorData.push(
...c,
...c,
)
} else {
// prettier-ignore
colorData.push(
...color,
...color,
)
}
}
}
// Vertex buffer
this.vertexBuffer = gpu.mapBuffer(device, new Float32Array(vertexData), {
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// Color buffer
this.colorBuffer = gpu.mapBuffer(device, new Float32Array(colorData), {
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// Uniform buffer for transformation matrix
this.uniformBuffer = device.createBuffer({
size: 64, // 4x4 matrix = 16 floats = 64 bytes
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({ code: vertexShader }),
entryPoint: 'vs',
buffers: [
{
// Position buffer layout
arrayStride: 16, // 4 floats * 4 bytes
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x4',
},
],
},
{
// Color buffer layout
arrayStride: 16, // 4 floats * 4 bytes
attributes: [
{
shaderLocation: 1,
offset: 0,
format: 'float32x4',
},
],
},
],
},
fragment: {
module: device.createShaderModule({ code: fragmentShader }),
entryPoint: 'fs',
targets: [
{
format: navigator.gpu.getPreferredCanvasFormat(),
blend: gpu.Blending.Additive,
},
],
},
primitive: {
topology: 'line-list',
},
multisample: {
count: 4,
},
});
this.bindGroup = device.createBindGroup({
layout: this.pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: { buffer: this.uniformBuffer },
},
],
});
this.vertexCount = vertexData.length / 4;
}
render(camera) {
this.device.queue.writeBuffer(this.uniformBuffer, 0, camera);
const commandEncoder = this.device.createCommandEncoder();
const canvasTexture = this.context.getCurrentTexture();
// Antialiasing
// https://webgpufundamentals.org/webgpu/lessons/webgpu-multisampling.html
if (!this.multisampleTexture ||
this.multisampleTexture.width !== canvasTexture.width ||
this.multisampleTexture.height !== canvasTexture.height) {
if (this.multisampleTexture) {
this.multisampleTexture.destroy();
}
this.multisampleTexture = this.device.createTexture({
format: canvasTexture.format,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
size: [canvasTexture.width, canvasTexture.height],
sampleCount: 4,
});
}
this.renderPassDescriptor.colorAttachments[0].view = this.multisampleTexture.createView();
this.renderPassDescriptor.colorAttachments[0].resolveTarget = canvasTexture.createView();
const renderPass = commandEncoder.beginRenderPass(this.renderPassDescriptor);
renderPass.setPipeline(this.pipeline);
renderPass.setBindGroup(0, this.bindGroup);
renderPass.setVertexBuffer(0, this.vertexBuffer);
renderPass.setVertexBuffer(1, this.colorBuffer);
renderPass.draw(this.vertexCount);
renderPass.end();
this.device.queue.submit([commandEncoder.finish()]);
}
cleanup() {
;[this.vertexBuffer, this.colorBuffer, this.uniformBuffer].forEach(buffer => buffer.destroy());
}
}
return LineRenderer
}