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

705 lines
19 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 Blend</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%;
}
canvas {
background-color: #404040;
background-image:
linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 32px 32px;
background-position: 0 0, 0 16px, 16px -16px, -16px 0px;
}
</style>
</head>
<body>
<canvas></canvas>
</body>
<script type="module">
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#wgpu-matrix
import {mat4} from '../3rdparty/wgpu-matrix.module.js';
import GUI from '../3rdparty/muigui-0.x.module.js';
const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
const hsla = (h, s, l, a) => `hsla(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%, ${a})`;
function createSourceImage(size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.translate(size / 2, size / 2);
ctx.globalCompositeOperation = 'screen';
const numCircles = 3;
for (let i = 0; i < numCircles; ++i) {
ctx.rotate(Math.PI * 2 / numCircles);
ctx.save();
ctx.translate(size / 6, 0);
ctx.beginPath();
const radius = size / 3;
ctx.arc(0, 0, radius, 0, Math.PI * 2);
const gradient = ctx.createRadialGradient(0, 0, radius / 2, 0, 0, radius);
const h = i / numCircles;
gradient.addColorStop(0.5, hsla(h, 1, 0.5, 1));
gradient.addColorStop(1, hsla(h, 1, 0.5, 0));
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
return canvas;
}
function createDestinationImage(size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, size, size);
for (let i = 0; i <= 6; ++i) {
gradient.addColorStop(i / 6, hsl(i / -6, 1, 0.5));
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = 'rgba(0, 0, 0, 255)';
ctx.globalCompositeOperation = 'destination-out';
ctx.rotate(Math.PI / -4);
for (let i = 0; i < size * 2; i += 32) {
ctx.fillRect(-size, i, size * 2, 16);
}
return canvas;
}
const size = 300;
const srcCanvas = createSourceImage(size);
const dstCanvas = createDestinationImage(size);
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();
const module = device.createShaderModule({
label: 'our hardcoded textured quad shaders',
code: /* wgsl */ `
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
struct Uniforms {
matrix: mat4x4f,
};
@group(0) @binding(2) var<uniform> uni: Uniforms;
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.0), // center
vec2f( 1.0, 0.0), // right, center
vec2f( 0.0, 1.0), // center, top
// 2st triangle
vec2f( 0.0, 1.0), // center, top
vec2f( 1.0, 0.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
var vsOutput: OurVertexShaderOutput;
let xy = pos[vertexIndex];
vsOutput.position = uni.matrix * vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy;
return vsOutput;
}
@group(0) @binding(0) var ourSampler: sampler;
@group(0) @binding(1) var ourTexture: texture_2d<f32>;
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return textureSample(ourTexture, ourSampler, fsInput.texcoord);
}
`,
});
const numMipLevels = (...sizes) => {
const maxSize = Math.max(...sizes);
return 1 + Math.log2(maxSize) | 0;
};
function copySourceToTexture(device, texture, source, {flipY, premultipliedAlpha} = {}) {
device.queue.copyExternalImageToTexture(
{ source, flipY, },
{ texture, premultipliedAlpha },
{ width: source.width, height: source.height },
);
if (texture.mipLevelCount > 1) {
generateMips(device, texture);
}
}
function createTextureFromSource(device, source, options = {}) {
const texture = device.createTexture({
format: 'rgba8unorm',
mipLevelCount: options.mips ? numMipLevels(source.width, source.height) : 1,
size: [source.width, source.height],
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
copySourceToTexture(device, texture, source, options);
return texture;
}
const generateMips = (() => {
let sampler;
let module;
const pipelineByFormat = {};
return function generateMips(device, texture) {
if (!module) {
module = device.createShaderModule({
label: 'textured quad shaders for mip level generation',
code: /* wgsl */ `
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> VSOutput {
let pos = array(
vec2f( 0.0, 0.0), // center
vec2f( 1.0, 0.0), // right, center
vec2f( 0.0, 1.0), // center, top
// 2st triangle
vec2f( 0.0, 1.0), // center, top
vec2f( 1.0, 0.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
var vsOutput: VSOutput;
let xy = pos[vertexIndex];
vsOutput.position = vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
vsOutput.texcoord = vec2f(xy.x, 1.0 - xy.y);
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);
}
`,
});
sampler = device.createSampler({
minFilter: 'linear',
});
}
if (!pipelineByFormat[texture.format]) {
pipelineByFormat[texture.format] = device.createRenderPipeline({
label: 'mip level generator pipeline',
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
targets: [{ format: texture.format }],
},
});
}
const pipeline = pipelineByFormat[texture.format];
const encoder = device.createCommandEncoder({
label: 'mip gen encoder',
});
for (let baseMipLevel = 1; baseMipLevel < texture.mipLevelCount; ++baseMipLevel) {
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{
binding: 1,
resource: texture.createView({
baseMipLevel: baseMipLevel - 1,
mipLevelCount: 1,
}),
},
],
});
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
view: texture.createView({
baseMipLevel,
mipLevelCount: 1,
}),
loadOp: 'clear',
storeOp: 'store',
},
],
};
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(6); // call our vertex shader 6 times
pass.end();
}
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
};
})();
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { }, },
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { } },
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { } },
],
});
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [
bindGroupLayout,
],
});
const srcTextureUnpremultipliedAlpha =
createTextureFromSource(
device, srcCanvas,
{mips: true});
const dstTextureUnpremultipliedAlpha =
createTextureFromSource(
device, dstCanvas,
{mips: true});
const srcTexturePremultipliedAlpha =
createTextureFromSource(
device, srcCanvas,
{mips: true, premultipliedAlpha: true});
const dstTexturePremultipliedAlpha =
createTextureFromSource(
device, dstCanvas,
{mips: true, premultipliedAlpha: true});
const sampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
});
function makeUniformBufferAndValues(device) {
// offsets to the various uniform values in float32 indices
const kMatrixOffset = 0;
// create a buffer for the uniform values
const uniformBufferSize =
16 * 4; // matrix is 16 32bit floats (4bytes each)
const buffer = 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 values = new Float32Array(uniformBufferSize / 4);
const matrix = values.subarray(kMatrixOffset, 16);
return { buffer, values, matrix };
}
const srcUniform = makeUniformBufferAndValues(device);
const dstUniform = makeUniformBufferAndValues(device);
const srcBindGroupUnpremultipliedAlpha = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: srcTextureUnpremultipliedAlpha },
{ binding: 2, resource: { buffer: srcUniform.buffer }},
],
});
const dstBindGroupUnpremultipliedAlpha = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: dstTextureUnpremultipliedAlpha },
{ binding: 2, resource: { buffer: dstUniform.buffer }},
],
});
const srcBindGroupPremultipliedAlpha = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: srcTexturePremultipliedAlpha },
{ binding: 2, resource: { buffer: srcUniform.buffer }},
],
});
const dstBindGroupPremultipliedAlpha = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: dstTexturePremultipliedAlpha },
{ binding: 2, resource: { buffer: dstUniform.buffer }},
],
});
const textureSets = [
{
srcTexture: srcTexturePremultipliedAlpha,
dstTexture: dstTexturePremultipliedAlpha,
srcBindGroup: srcBindGroupPremultipliedAlpha,
dstBindGroup: dstBindGroupPremultipliedAlpha,
},
{
srcTexture: srcTextureUnpremultipliedAlpha,
dstTexture: dstTextureUnpremultipliedAlpha,
srcBindGroup: srcBindGroupUnpremultipliedAlpha,
dstBindGroup: dstBindGroupUnpremultipliedAlpha,
},
];
const clearValue = [0, 0, 0, 0];
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue,
loadOp: 'clear',
storeOp: 'store',
},
],
};
const operations = [
'add',
'subtract',
'reverse-subtract',
'min',
'max',
];
const factors = [
'zero',
'one',
'src',
'one-minus-src',
'src-alpha',
'one-minus-src-alpha',
'dst',
'one-minus-dst',
'dst-alpha',
'one-minus-dst-alpha',
'src-alpha-saturated',
'constant',
'one-minus-constant',
];
const presets = {
'default (copy)': {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero',
},
},
'premultiplied blend (source-over)': {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
},
'un-premultiplied blend': {
color: {
operation: 'add',
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
},
},
'destination-over': {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'one',
},
},
'source-in': {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'zero',
},
},
'destination-in': {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'src-alpha',
},
},
'source-out': {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'zero',
},
},
'destination-out': {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
},
},
'source-atop': {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha',
},
},
'destination-atop': {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'src-alpha',
},
},
'additive (lighten)': {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one',
},
},
};
const color = {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src',
};
const alpha = {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src',
};
const constant = {
color: [1, 0.5, 0.25],
alpha: 1,
};
const clear = {
color: [0, 0, 0],
alpha: 0,
premultiply: true,
};
const settings = {
alphaMode: 'premultiplied',
textureSet: 0,
preset: 'default (copy)',
};
const gui = new GUI().onChange(render);
gui.add(settings, 'alphaMode', ['opaque', 'premultiplied']).name('canvas alphaMode');
gui.add(settings, 'textureSet', ['premultiplied alpha', 'un-premultiplied alpha']);
gui.add(settings, 'preset', Object.keys(presets))
.name('blending preset')
.onChange(presetName => {
const preset = presets[presetName];
Object.assign(color, preset.color);
Object.assign(alpha, preset.alpha || preset.color);
gui.updateDisplay();
});
const colorFolder = gui.addFolder('color');
colorFolder.add(color, 'operation', operations);
colorFolder.add(color, 'srcFactor', factors);
colorFolder.add(color, 'dstFactor', factors);
const alphaFolder = gui.addFolder('alpha');
alphaFolder.add(alpha, 'operation', operations);
alphaFolder.add(alpha, 'srcFactor', factors);
alphaFolder.add(alpha, 'dstFactor', factors);
const constantFolder = gui.addFolder('constant');
constantFolder.addColor(constant, 'color');
constantFolder.add(constant, 'alpha', 0, 1);
const clearFolder = gui.addFolder('clear color');
clearFolder.add(clear, 'premultiply');
clearFolder.add(clear, 'alpha', 0, 1);
clearFolder.addColor(clear, 'color');
const dstPipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: pipelineLayout,
vertex: {
module,
},
fragment: {
module,
targets: [ { format: presentationFormat } ],
},
});
function makeBlendComponentValid(blend) {
const { operation } = blend;
if (operation === 'min' || operation === 'max') {
blend.srcFactor = 'one';
blend.dstFactor = 'one';
}
}
function render() {
makeBlendComponentValid(color);
makeBlendComponentValid(alpha);
gui.updateDisplay();
const srcPipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: pipelineLayout,
vertex: {
module,
},
fragment: {
module,
targets: [
{
format: presentationFormat,
blend: {
color,
alpha,
},
},
],
},
});
const {
srcTexture,
dstTexture,
srcBindGroup,
dstBindGroup,
} = textureSets[settings.textureSet];
context.configure({
device,
format: presentationFormat,
alphaMode: settings.alphaMode,
});
const canvasTexture = context.getCurrentTexture();
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view =
canvasTexture.createView();
{
const { alpha, color, premultiply } = clear;
const mult = premultiply ? alpha : 1;
clearValue[0] = color[0] * mult;
clearValue[1] = color[1] * mult;
clearValue[2] = color[2] * mult;
clearValue[3] = alpha;
}
function updateUniforms(uniform, canvasTexture, texture) {
const projectionMatrix = mat4.ortho(0, canvasTexture.width, canvasTexture.height, 0, -1, 1);
mat4.scale(projectionMatrix, [texture.width, texture.height, 1], uniform.matrix);
// copy the values from JavaScript to the GPU
device.queue.writeBuffer(uniform.buffer, 0, uniform.values);
}
updateUniforms(srcUniform, canvasTexture, srcTexture);
updateUniforms(dstUniform, canvasTexture, dstTexture);
const encoder = device.createCommandEncoder({ label: 'render quad encoder' });
const pass = encoder.beginRenderPass(renderPassDescriptor);
// draw dst
pass.setPipeline(dstPipeline);
pass.setBindGroup(0, dstBindGroup);
pass.draw(6); // call our vertex shader 6 times
// draw src
pass.setPipeline(srcPipeline);
pass.setBindGroup(0, srcBindGroup);
pass.setBlendConstant([...constant.color, constant.alpha]);
pass.draw(6); // call our vertex shader 6 times
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
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));
render();
}
});
observer.observe(canvas);
}
function fail(msg) {
// eslint-disable-next-line no-alert
alert(msg);
}
main();
</script>
</html>