Files
webgpufundamentals/webgpu/webgpu-post-processing-step-03.html
Gregg Tavares dcfd514975 Revert "Use direct attachment binding"
This reverts commit bdce233195.
2026-02-04 12:17:44 -08:00

467 lines
13 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGPU Post Processing - Step 3 - rgb elements</title>
<style>
@import url(resources/webgpu-lesson.css);
html, body {
margin: 0; /* remove the default margin */
height: 100%; /* make the html,body fill the page */
}
canvas {
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
</style>
</head>
<body>
<canvas></canvas>
</body>
<script type="module">
import GUI from '../3rdparty/muigui-0.x.module.js';
// A random number between [min and max)
// With 1 argument it will be [0 to min)
// With no arguments it will be [0 to 1)
const rand = (min, max) => {
if (min === undefined) {
min = 0;
max = 1;
} else if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
};
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {}) {
// 2 triangles per subdivision, 3 verts per tri
const numVertices = numSubdivisions * 3 * 2;
// 2 32-bit values for position (xy) and 1 32-bit value for color (rgb_)
// The 32-bit color value will be written/read as 4 8-bit values
const vertexData = new Float32Array(numVertices * (2 + 1));
const colorData = new Uint8Array(vertexData.buffer);
let offset = 0;
let colorOffset = 8;
const addVertex = (x, y, r, g, b) => {
vertexData[offset++] = x;
vertexData[offset++] = y;
offset += 1; // skip the color
colorData[colorOffset++] = r * 255;
colorData[colorOffset++] = g * 255;
colorData[colorOffset++] = b * 255;
colorOffset += 9; // skip extra byte and the position
};
const innerColor = [1, 1, 1];
const outerColor = [0.1, 0.1, 0.1];
// 2 vertices per subdivision
//
// 0--1 4
// | / /|
// |/ / |
// 2 3--5
for (let i = 0; i < numSubdivisions; ++i) {
const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions;
const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions;
const c1 = Math.cos(angle1);
const s1 = Math.sin(angle1);
const c2 = Math.cos(angle2);
const s2 = Math.sin(angle2);
// first triangle
addVertex(c1 * radius, s1 * radius, ...outerColor);
addVertex(c2 * radius, s2 * radius, ...outerColor);
addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);
// second triangle
addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);
addVertex(c2 * radius, s2 * radius, ...outerColor);
addVertex(c2 * innerRadius, s2 * innerRadius, ...innerColor);
}
return {
vertexData,
numVertices,
};
}
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({
code: /* wgsl */ `
struct Vertex {
@location(0) position: vec2f,
@location(1) color: vec4f,
@location(2) offset: vec2f,
@location(3) scale: vec2f,
@location(4) perVertexColor: vec3f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@vertex fn vs(
vert: Vertex,
) -> VSOutput {
var vsOut: VSOutput;
vsOut.position = vec4f(
vert.position * vert.scale + vert.offset, 0.0, 1.0);
vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);
return vsOut;
}
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
return vsOut.color;
}
`,
});
const pipeline = device.createRenderPipeline({
label: 'per vertex color',
layout: 'auto',
vertex: {
module,
buffers: [
{
arrayStride: 2 * 4 + 4, // 2 floats, 4 bytes each + 4 bytes
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position
{shaderLocation: 4, offset: 8, format: 'unorm8x4'}, // perVertexColor
],
},
{
arrayStride: 4, // 4 bytes
stepMode: 'instance',
attributes: [
{shaderLocation: 1, offset: 0, format: 'unorm8x4'}, // color
],
},
{
arrayStride: 4 * 4, // 4 floats, 4 bytes each
stepMode: 'instance',
attributes: [
{shaderLocation: 2, offset: 0, format: 'float32x2'}, // offset
{shaderLocation: 3, offset: 8, format: 'float32x2'}, // scale
],
},
],
},
fragment: {
module,
targets: [{ format: 'rgba8unorm' }],
},
});
const kNumObjects = 10000;
const objectInfos = [];
// create 2 vertex buffers
const staticUnitSize =
4; // color is 4 bytes
const changingUnitSize =
2 * 4 + // offset is 2 32bit floats (4bytes each)
2 * 4; // scale is 2 32bit floats (4bytes each)
const staticVertexBufferSize = staticUnitSize * kNumObjects;
const changingVertexBufferSize = changingUnitSize * kNumObjects;
const staticVertexBuffer = device.createBuffer({
label: 'static vertex for objects',
size: staticVertexBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const changingVertexBuffer = device.createBuffer({
label: 'changing storage for objects',
size: changingVertexBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// offsets to the various uniform values in float32 indices
const kColorOffset = 0;
const kOffsetOffset = 0;
const kScaleOffset = 2;
{
const staticVertexValuesU8 = new Uint8Array(staticVertexBufferSize);
for (let i = 0; i < kNumObjects; ++i) {
const staticOffsetU8 = i * staticUnitSize;
// These are only set once so set them now
staticVertexValuesU8.set( // set the color
[rand() * 255, rand() * 255, rand() * 255, 255],
staticOffsetU8 + kColorOffset);
objectInfos.push({
scale: rand(0.2, 0.5),
offset: [rand(-0.9, 0.9), rand(-0.9, 0.9)],
velocity: [rand(-0.1, 0.1), rand(-0.1, 0.1)],
});
}
device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValuesU8);
}
// a typed array we can use to update the changingStorageBuffer
const vertexValues = new Float32Array(changingVertexBufferSize / 4);
const { vertexData, numVertices } = createCircleVertices({
radius: 0.5,
innerRadius: 0.25,
});
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
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',
},
],
};
const postProcessModule = device.createShaderModule({
code: /* wgsl */ `
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32,
) -> VSOutput {
var pos = array(
vec2f(-1.0, -1.0),
vec2f(-1.0, 3.0),
vec2f( 3.0, -1.0),
);
var vsOutput: VSOutput;
let xy = pos[vertexIndex];
vsOutput.position = vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5);
return vsOutput;
}
struct Uniforms {
effectAmount: f32,
bandMult: f32,
cellMult: f32,
cellBright: f32,
};
@group(0) @binding(0) var postTexture2d: texture_2d<f32>;
@group(0) @binding(1) var postSampler: sampler;
@group(0) @binding(2) var<uniform> uni: Uniforms;
@fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f {
let banding = abs(sin(fsInput.position.y * uni.bandMult));
let cellNdx = u32(fsInput.position.x * uni.cellMult) % 3;
var cellColor = vec3f(0);
cellColor[cellNdx] = 1.0;
let cMult = cellColor + uni.cellBright;
let effect = mix(vec3f(1), banding * cMult, uni.effectAmount);
let color = textureSample(postTexture2d, postSampler, fsInput.texcoord);
return vec4f(color.rgb * effect, color.a);
}
`,
});
const postProcessPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: postProcessModule },
fragment: {
module: postProcessModule,
targets: [ { format: presentationFormat }],
},
});
const postProcessSampler = device.createSampler({
minFilter: 'linear',
magFilter: 'linear',
});
const postProcessRenderPassDescriptor = {
label: 'post process render pass',
colorAttachments: [
{ loadOp: 'clear', storeOp: 'store' },
],
};
const postProcessUniformBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
let renderTarget;
let postProcessBindGroup;
function setupPostProcess(canvasTexture) {
if (renderTarget?.width === canvasTexture.width &&
renderTarget?.height === canvasTexture.height) {
return;
}
renderTarget?.destroy();
renderTarget = device.createTexture({
size: canvasTexture,
format: 'rgba8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
});
const renderTargetView = renderTarget.createView();
renderPassDescriptor.colorAttachments[0].view = renderTargetView;
postProcessBindGroup = device.createBindGroup({
layout: postProcessPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: renderTargetView },
{ binding: 1, resource: postProcessSampler },
{ binding: 2, resource: postProcessUniformBuffer },
],
});
}
function postProcess(encoder, srcTexture, dstTexture) {
device.queue.writeBuffer(
postProcessUniformBuffer,
0,
new Float32Array([
settings.affectAmount,
settings.bandMult,
settings.cellMult,
settings.cellBright,
]),
);
postProcessRenderPassDescriptor.colorAttachments[0].view = dstTexture.createView();
const pass = encoder.beginRenderPass(postProcessRenderPassDescriptor);
pass.setPipeline(postProcessPipeline);
pass.setBindGroup(0, postProcessBindGroup);
pass.draw(3);
pass.end();
}
const settings = {
numObjects: 200,
affectAmount: 1,
bandMult: 1,
cellMult: 0.5,
cellBright: 1,
};
const gui = new GUI();
gui.add(settings, 'affectAmount', 0, 1);
gui.add(settings, 'bandMult', 0.01, 2.0);
gui.add(settings, 'cellMult', 0, 1);
gui.add(settings, 'cellBright', 0, 2);
const euclideanModulo = (x, a) => x - a * Math.floor(x / a);
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
const canvasTexture = context.getCurrentTexture();
setupPostProcess(canvasTexture);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setVertexBuffer(1, staticVertexBuffer);
pass.setVertexBuffer(2, changingVertexBuffer);
// Set the uniform values in our JavaScript side Float32Array
const aspect = canvas.width / canvas.height;
// set the scales for each object
for (let ndx = 0; ndx < settings.numObjects; ++ndx) {
const {scale, offset, velocity} = objectInfos[ndx];
// -1.5 to 1.5
offset[0] = euclideanModulo(offset[0] + velocity[0] * deltaTime + 1.5, 3) - 1.5;
offset[1] = euclideanModulo(offset[1] + velocity[1] * deltaTime + 1.5, 3) - 1.5;
const off = ndx * (changingUnitSize / 4);
vertexValues.set(offset, off + kOffsetOffset);
vertexValues.set([scale / aspect, scale], off + kScaleOffset);
}
// upload all offsets and scales at once
device.queue.writeBuffer(
changingVertexBuffer, 0,
vertexValues, 0, settings.numObjects * changingUnitSize / 4);
pass.draw(numVertices, settings.numObjects);
pass.end();
postProcess(encoder, renderTarget, canvasTexture);
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
}
});
observer.observe(canvas);
}
function fail(msg) {
alert(msg);
}
main();
</script>
</html>