plasma-arc / index.html
p3nGu1nZz's picture
🐈
c3695de
raw
history blame
12.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGPU Text Rendering</title>
</head>
<body>
<canvas></canvas>
<script type="module">
// WebGPU Simple Textured Quad - Import Canvas
// from https://webgpufundamentals.org/webgpu/webgpu-simple-textured-quad-import-canvas.html
import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
const glyphWidth = 32;
const glyphHeight = 40;
const glyphsAcrossTexture = 16;
function genreateGlyphTextureAtlas() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.font = '32px monospace';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillStyle = 'white';
for (let c = 33, x = 0, y = 0; c < 128; ++c) {
ctx.fillText(String.fromCodePoint(c), x + glyphWidth / 2, y + glyphHeight / 2);
x = (x + glyphWidth) % canvas.width;
if (x === 0) y += glyphHeight;
}
return canvas;
}
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// Get a WebGPU context from the canvas and configure it
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({
label: 'our hardcoded textured quad shaders',
code: `
struct VSInput {
@location(0) position: vec4f,
@location(1) texcoord: vec2f,
@location(2) color: vec4f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
@location(1) color: vec4f,
};
struct Uniforms {
matrix: mat4x4f,
};
@group(0) @binding(2) var<uniform> uni: Uniforms;
@vertex fn vs(vin: VSInput) -> VSOutput {
var vsOutput: VSOutput;
vsOutput.position = uni.matrix * vin.position;
vsOutput.texcoord = vin.texcoord;
vsOutput.color = vin.color;
return vsOutput;
}
@group(0) @binding(0) var ourSampler: sampler;
@group(0) @binding(1) var ourTexture: texture_2d<f32>;
@fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(ourTexture, ourSampler, fsInput.texcoord) * fsInput.color;
}
`,
});
const glyphCanvas = genreateGlyphTextureAtlas();
// so we can see it
document.body.appendChild(glyphCanvas);
glyphCanvas.style.backgroundColor = '#222';
const maxGlyphs = 100;
const floatsPerVertex = 2 + 2 + 4; // 2(pos) + 2(texcoord) + 4(color)
const vertexSize = floatsPerVertex * 4; // 4 bytes each float
const vertsPerGlyph = 6;
const vertexBufferSize = maxGlyphs * vertsPerGlyph * vertexSize;
const vertexBuffer = device.createBuffer({
label: 'vertices',
size: vertexBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const indexBuffer = device.createBuffer({
label: 'indices',
size: maxGlyphs * vertsPerGlyph * 4,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
// pre fill index buffer with quad indices
{
const indices = [];
for (let i = 0; i < maxGlyphs; ++i) {
const ndx = i * 4;
indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3);
}
device.queue.writeBuffer(indexBuffer, 0, new Uint32Array(indices));
}
function generateGlyphVerticesForText(s, colors = [[1, 1, 1, 1]]) {
const vertexData = new Float32Array(maxGlyphs * floatsPerVertex * vertsPerGlyph);
const glyphUVWidth = glyphWidth / glyphCanvas.width;
const glyphUVHeight = glyphHeight / glyphCanvas.height;
let offset = 0, x0 = 0, y0 = 0, x1 = 1, y1 = 1, width = 0;
let colorNdx = 0;
const addVertex = (x, y, u, v, r, g, b, a) => {
vertexData.set([x, y, u, v, r, g, b, a], offset);
offset += 8;
};
for (let i = 0; i < s.length; ++i) {
const c = s.charCodeAt(i);
if (c >= 33) {
const glyphX = (c - 33) % glyphsAcrossTexture;
const glyphY = Math.floor((c - 33) / glyphsAcrossTexture);
const u0 = (glyphX * glyphWidth) / glyphCanvas.width;
const v0 = ((glyphY + 1) * glyphHeight) / glyphCanvas.height;
const u1 = u0 + glyphUVWidth;
const v1 = v0 - glyphUVHeight;
width = Math.max(x1, width);
addVertex(x0, y0, u0, v0, ...colors[colorNdx]);
addVertex(x1, y0, u1, v0, ...colors[colorNdx]);
addVertex(x0, y1, u0, v1, ...colors[colorNdx]);
addVertex(x1, y1, u1, v1, ...colors[colorNdx]);
} else {
colorNdx = (colorNdx + 1) % colors.length;
if (c === 10) {
x0 = 0; x1 = 1; y0--; y1 = y0 + 1;
continue;
}
}
x0 += 0.55; x1 = x0 + 1;
}
return { vertexData, numGlyphs: offset / floatsPerVertex, width, height: y1 };
}
const { vertexData, numGlyphs, width, height } = generateGlyphVerticesForText(
'Hello\nworld!\nText in\nWebGPU!', [
[1, 1, 0, 1], [0, 1, 1, 1], [1, 0, 1, 1], [1, 0, 0, 1], [0, .5, 1, 1],
]);
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
const pipeline = device.createRenderPipeline({
label: 'textured quad pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
arrayStride: vertexSize,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // position
{ shaderLocation: 1, offset: 8, format: 'float32x2' }, // texcoord
{ shaderLocation: 2, offset: 16, format: 'float32x4' } // color
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format: presentationFormat }],
},
});
function copySourceToTexture(device, texture, source, { flipY } = {}) {
device.queue.copyExternalImageToTexture(
{ source, flipY, },
{ texture, premultipliedAlpha: true },
{ width: source.width, height: source.height },
);
}
function createTextureFromSource(device, source, options = {}) {
const texture = device.createTexture({
format: 'rgba8unorm',
size: [source.width, source.height],
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
copySourceToTexture(device, texture, source, options);
return texture;
}
const texture = createTextureFromSource(device, glyphCanvas, { mips: true });
const sampler = device.createSampler({
minFilter: 'linear',
magFilter: 'linear',
});
// create a buffer for the uniform values
const uniformBufferSize =
16 * 4; // matrix is 16 32bit floats (4bytes each)
const uniformBuffer = device.createBuffer({
label: 'uniforms for quad',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// create a typedarray to hold the values for the uniforms in JavaScript
const kMatrixOffset = 0;
const uniformValues = new Float32Array(uniformBufferSize / 4);
const matrix = uniformValues.subarray(kMatrixOffset, 16);
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: texture.createView() },
{ binding: 2, resource: { buffer: uniformBuffer } },
],
});
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
};
function render(time) {
time *= 0.001;
const fov = 60 * Math.PI / 180; // 60 degrees in radians
const aspect = canvas.clientWidth / canvas.clientHeight;
const zNear = 0.001;
const zFar = 50;
const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);
const cameraPosition = [0, 0, 5];
const up = [0, 1, 0];
const target = [0, 0, 0];
const viewMatrix = mat4.lookAt(cameraPosition, target, up);
const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder({
label: 'render quad encoder',
});
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
mat4.rotateY(viewProjectionMatrix, time, matrix);
mat4.translate(matrix, [-width / 2, -height / 2, 0], matrix);
// copy the values from JavaScript to the GPU
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setBindGroup(0, bindGroup);
pass.setVertexBuffer(0, vertexBuffer);
pass.setIndexBuffer(indexBuffer, 'uint32');
pass.drawIndexed(numGlyphs * 6);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function fail(msg) {
// eslint-disable-next-line no-alert
alert(msg);
}
main();
</script>
</body>
</html>