Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Plasma-Arc: WGPU Experiment</title> | |
</head> | |
<body> | |
<canvas></canvas> | |
<script type="module"> | |
import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js'; | |
import { fetchShaderCode, generateGlyphTextureAtlas } from './utility.js'; | |
import { CONFIG } from './config.js'; | |
import { CANVAS, CTX, COLORS, RENDER_PASS_DESCRIPTOR } from './constants.js'; | |
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(CANVAS, CTX, CONFIG); | |
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 | |
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 { 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' }, // pos | |
{ shaderLocation: 1, offset: 8, format: 'float32x2' }, // tex | |
{ shaderLocation: 2, offset: 16, format: 'float32x4' } // col | |
], | |
}, | |
], | |
}, | |
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 uniformBuffer = device.createBuffer({ | |
label: 'uniforms for quad', | |
size: CONFIG.uniformBufferSize, | |
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | |
}); | |
const uniformValues = new Float32Array(CONFIG.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 } }, | |
], | |
}); | |
function render(time) { | |
time *= CONFIG.time.phase; | |
const fov = 60 * Math.PI / 180; | |
const aspect = canvas.clientWidth / canvas.clientHeight; | |
const projectionMatrix = mat4.perspective(fov, aspect, CONFIG.render.zNear, CONFIG.render.zFar); | |
const viewMatrix = mat4.lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]); | |
const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix); | |
RENDER_PASS_DESCRIPTOR.colorAttachments[0].view = context.getCurrentTexture().createView(); | |
const encoder = device.createCommandEncoder(); | |
const pass = encoder.beginRenderPass(RENDER_PASS_DESCRIPTOR); | |
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> |