plasma-arc / index.html
p3nGu1nZz's picture
add to config
c75754c
raw
history blame
10.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plasma-Arc: WebGPU Experiment</title>
</head>
<body>
<canvas></canvas>
<script type="module">
import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
import { fetchShaderCode } from './utility.js';
import { config } from './config.js';
function generateGlyphTextureAtlas() {
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 + config.glyphWidth / 2, y + config.glyphHeight / 2);
x = (x + config.glyphWidth) % canvas.width;
if (x === 0) y += config.glyphHeight;
}
return canvas;
}
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
alert('need a browser that supports WebGPU');
return;
}
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const shaderCode = await fetchShaderCode('shaders.wgsl');
const module = device.createShaderModule({
label: 'textured quad shaders',
code: shaderCode,
});
const glyphCanvas = generateGlyphTextureAtlas();
document.body.appendChild(glyphCanvas);
glyphCanvas.style.backgroundColor = '#222';
const vertexSize = config.floatsPerVertex * 4;
const vertexBufferSize = config.maxGlyphs * config.vertsPerGlyph * vertexSize;
const vertexBuffer = device.createBuffer({
label: 'vertices',
size: vertexBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const indexBuffer = device.createBuffer({
label: 'indices',
size: config.maxGlyphs * config.vertsPerGlyph * 4,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
const indices = Array.from({ length: config.maxGlyphs * 6 }, (_, i) => {
const ndx = Math.floor(i / 6) * 4;
return (i % 6 < 3 ? [ndx, ndx + 1, ndx + 2] : [ndx + 2, ndx + 1, ndx + 3])[i % 3];
});
device.queue.writeBuffer(indexBuffer, 0, new Uint32Array(indices));
function generateGlyphVerticesForText(s, colors = [[1, 1, 1, 1]]) {
const vertexData = new Float32Array(config.maxGlyphs * config.floatsPerVertex * config.vertsPerGlyph);
const glyphUVWidth = config.glyphWidth / glyphCanvas.width;
const glyphUVHeight = config.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, color) => {
vertexData.set([x, y, u, v, ...color], offset);
offset += 8;
};
for (let i = 0; i < s.length; ++i) {
const c = s.charCodeAt(i);
if (c >= 33) {
const cIndex = c - 33;
const glyphX = cIndex % config.glyphsAcrossTexture;
const glyphY = Math.floor(cIndex / config.glyphsAcrossTexture);
const u0 = glyphX * config.glyphWidth / glyphCanvas.width;
const v1 = glyphY * config.glyphHeight / glyphCanvas.height;
const u1 = u0 + glyphUVWidth;
const v0 = v1 + 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) { // Newline character
x0 = 0; x1 = 1; y0--; y1 = y0 + 1;
continue;
}
}
x0 += 0.55; x1 = x0 + 1;
}
return { vertexData, numGlyphs: offset / config.floatsPerVertex, width, height: y1 };
}
const colors = [
[1, 1, 0, 1],
[0, 1, 1, 1],
[1, 0, 1, 1],
[1, 0, 0, 1],
[0, .5, 1, 1]
];
const { vertexData, numGlyphs, width, height } = generateGlyphVerticesForText('Hello\nworld!\nText in\nWebGPU!', colors);
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,
blend: {
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }
},
}],
},
});
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,
});
device.queue.copyExternalImageToTexture(
{ source, flipY: options.flipY },
{ texture, premultipliedAlpha: true },
{ width: source.width, height: source.height }
);
return texture;
}
const texture = createTextureFromSource(device, glyphCanvas, { mips: true });
const sampler = device.createSampler({
minFilter: 'linear',
magFilter: 'linear'
});
const uniformBufferSize = 16 * 4; //f32
const uniformBuffer = device.createBuffer({
label: 'uniforms for quad',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
const uniformValues = new Float32Array(uniformBufferSize / 4);
const matrix = uniformValues.subarray(0, 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: 'canvas render pass',
colorAttachments: [{
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
}],
};
function render(time) {
time *= 0.001;
const fov = 60 * Math.PI / 180;
const aspect = canvas.clientWidth / canvas.clientHeight;
const zNear = 0.001, zFar = 50;
const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);
const viewMatrix = mat4.lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]);
const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
mat4.rotateY(viewProjectionMatrix, time, matrix);
mat4.translate(matrix, [-width / 2, -height / 2, 0], matrix);
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setBindGroup(0, bindGroup);
pass.setVertexBuffer(0, vertexBuffer);
pass.setIndexBuffer(indexBuffer, 'uint32');
pass.drawIndexed(numGlyphs * 6);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
</body>
</html>