Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alphaToCoverage sample #444

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/tools/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ copyAndWatch(
{ src: 'sample/**/*', dst: 'out' },
{ src: 'samples/**/*', dst: 'out' },
{ src: 'shaders/**/*', dst: 'out' },
{ src: 'third_party/**/*', dst: 'out' },
{ src: 'index.html', dst: 'out' },
],
{ watch }
Expand Down
38 changes: 38 additions & 0 deletions sample/alphaToCoverage/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>webgpu-samples: alphaToCoverage</title>
<style>
:root {
color-scheme: light dark;
}
html, body {
margin: 0; /* remove default margin */
height: 100%; /* make body fill the browser window */
display: flex;
place-content: center center;
}
canvas {
width: min(100vw, 100vh);
height: min(100vw, 100vh);
display: block;
}
#imggif {
position: fixed;
right: 0;
bottom: 0;
width: min(20vw, 20vh);
height: min(20vw, 20vh);
}
</style>
<script src="../../third_party/gif.js/gif.js"></script>
<script defer type="module" src="main.js"></script>
<script defer type="module" src="../../js/iframe-helper.js"></script>
</head>
<body>
<canvas></canvas>
<img id=imggif></img>
</body>
</html>
241 changes: 241 additions & 0 deletions sample/alphaToCoverage/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { GUI } from 'dat.gui';

import showMultisampleTextureWGSL from './showMultisampleTexture.wgsl';
import renderWithAlphaToCoverageWGSL from './renderWithAlphaToCoverage.wgsl';
import { quitIfWebGPUNotAvailable } from '../util';

/* eslint @typescript-eslint/no-explicit-any: "off" */
declare const GIF: any; // from gif.js

const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
quitIfWebGPUNotAvailable(adapter, device);

//
// GUI controls
//

const kAlphaSteps = 64;

const kInitConfig = {
width: 8,
height: 8,
alpha: 4,
pause: false,
};
const config = { ...kInitConfig };
const updateConfig = () => {
const data = new Float32Array([config.alpha / kAlphaSteps]);
device.queue.writeBuffer(bufConfig, 0, data);
};

const gui = new GUI();
{
const buttons = {
initial() {
Object.assign(config, kInitConfig);
gui.updateDisplay();
},
captureGif() {
void captureGif();
},
};

const settings = gui.addFolder('Settings');
settings.open();
settings.add(config, 'width', 1, 16, 1);
settings.add(config, 'height', 1, 16, 1);

const alphaPanel = gui.addFolder('Alpha');
alphaPanel.open();
alphaPanel
.add(config, 'alpha', -2, kAlphaSteps + 2, 1)
.name(`alpha (of ${kAlphaSteps})`);
alphaPanel.add(config, 'pause', false);

gui.add(buttons, 'initial').name('reset to initial');
gui.add(buttons, 'captureGif').name('capture gif (right click gif to save)');
}

//
// Canvas setup
//

function updateCanvasSize() {
const devicePixelRatio = window.devicePixelRatio;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
}
updateCanvasSize();
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

const context = canvas.getContext('webgpu') as GPUCanvasContext;
context.configure({
device,
format: presentationFormat,
alphaMode: 'premultiplied',
});

//
// Config buffer
//

const bufConfig = device.createBuffer({
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
size: 128,
});

//
// Pipeline to render to a multisampled texture using alpha-to-coverage
//

const renderWithAlphaToCoverageModule = device.createShaderModule({
code: renderWithAlphaToCoverageWGSL,
});
const renderWithAlphaToCoveragePipeline = device.createRenderPipeline({
label: 'renderWithAlphaToCoveragePipeline',
layout: 'auto',
vertex: { module: renderWithAlphaToCoverageModule },
fragment: {
module: renderWithAlphaToCoverageModule,
targets: [{ format: 'rgba16float' }],
},
multisample: { count: 4, alphaToCoverageEnabled: true },
primitive: { topology: 'triangle-list' },
});
const renderWithAlphaToCoverageBGL =
renderWithAlphaToCoveragePipeline.getBindGroupLayout(0);

const renderWithAlphaToCoverageBG = device.createBindGroup({
layout: renderWithAlphaToCoverageBGL,
entries: [{ binding: 0, resource: { buffer: bufConfig } }],
});

//
// "Debug" view of the actual texture contents
//

const showMultisampleTextureModule = device.createShaderModule({
code: showMultisampleTextureWGSL,
});
const showMultisampleTexturePipeline = device.createRenderPipeline({
label: 'showMultisampleTexturePipeline',
layout: 'auto',
vertex: { module: showMultisampleTextureModule },
fragment: {
module: showMultisampleTextureModule,
targets: [{ format: presentationFormat }],
},
primitive: { topology: 'triangle-list' },
});
const showMultisampleTextureBGL =
showMultisampleTexturePipeline.getBindGroupLayout(0);

function render() {
updateConfig();

const multisampleTexture = device.createTexture({
format: 'rgba16float',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
size: [config.width, config.height],
sampleCount: 4,
});
const multisampleTextureView = multisampleTexture.createView();

const showMultisampleTextureBG = device.createBindGroup({
layout: showMultisampleTextureBGL,
entries: [{ binding: 0, resource: multisampleTextureView }],
});

const commandEncoder = device.createCommandEncoder();
// renderWithAlphaToCoverage pass
{
const pass = commandEncoder.beginRenderPass({
label: 'renderWithAlphaToCoverage pass',
colorAttachments: [
{
view: multisampleTextureView,
clearValue: [0, 0, 0, 1], // will be overwritten
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(renderWithAlphaToCoveragePipeline);
pass.setBindGroup(0, renderWithAlphaToCoverageBG);
pass.draw(6);
pass.end();
}
// showMultisampleTexture pass
{
const pass = commandEncoder.beginRenderPass({
label: 'showMulitsampleTexture pass',
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: [1, 0, 1, 1], // error color, will be overwritten
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(showMultisampleTexturePipeline);
pass.setBindGroup(0, showMultisampleTextureBG);
pass.draw(6);
pass.end();
}
device.queue.submit([commandEncoder.finish()]);

multisampleTexture.destroy();
}

function frame() {
if (!config.pause) {
config.alpha = ((performance.now() / 10000) % 1) * (kAlphaSteps + 4) - 2;
gui.updateDisplay();
}
updateCanvasSize();
render();
requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

async function captureGif() {
const size = Math.max(config.width, config.height) * 32;
const gif = new GIF({
workers: 4,
workerScript: '../../third_party/gif.js/gif.worker.js',
width: size,
height: size,
debug: true,
});

canvas.width = canvas.height = size;
const frames = [];
// Loop through all alpha values and render a frame
for (let alpha = 0; alpha <= kAlphaSteps; ++alpha) {
config.alpha = alpha;
render();
const dataURL = canvas.toDataURL();
// Only save the frame into the gif if it's different from the last one
if (dataURL !== frames[frames.length - 1]) {
frames.push(dataURL);
}
}
for (let i = 0; i < frames.length; ++i) {
const img = new Image();
img.src = frames[i];
gif.addFrame(img, {
delay: i == 0 || i == frames.length - 1 ? 2000 : 1000,
});
}

gif.on('finished', (blob) => {
const imggif = document.querySelector('#imggif') as HTMLImageElement;
imggif.src = URL.createObjectURL(blob);
console.log(imggif.src);
});
gif.render();
}
11 changes: 11 additions & 0 deletions sample/alphaToCoverage/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
name: 'Alpha-to-Coverage',
description:
'Visualizes how alpha-to-coverage translates alpha values into sample coverage on your device. This varies per device; for example, not all devices guarantee that once a sample pops in, it will stay; some devices repeat at 2x2 pixels, others at 4x4; etc. The circles show the 4 samples of each pixel; the background checkerboard shows where the pixels are.',
filename: __DIRNAME__,
sources: [
{ path: 'main.ts' },
{ path: './renderWithAlphaToCoverage.wgsl' },
{ path: './showMultisampleTexture.wgsl' },
],
};
26 changes: 26 additions & 0 deletions sample/alphaToCoverage/renderWithAlphaToCoverage.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
struct Config {
alpha: f32,
};
@group(0) @binding(0) var<uniform> config: Config;

struct Varying {
@builtin(position) pos: vec4f,
}

@vertex
fn vmain(
@builtin(vertex_index) vertex_index: u32,
) -> Varying {
var square = array(
vec2f(-1, -1), vec2f(-1, 1), vec2f( 1, -1),
vec2f( 1, -1), vec2f(-1, 1), vec2f( 1, 1),
);

return Varying(vec4(square[vertex_index], 0, 1));
}

@fragment
fn fmain(vary: Varying) -> @location(0) vec4f {
return vec4f(1, 1, 1, config.alpha);
}

58 changes: 58 additions & 0 deletions sample/alphaToCoverage/showMultisampleTexture.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@group(0) @binding(0) var tex: texture_multisampled_2d<f32>;

struct Varying {
@builtin(position) pos: vec4f,
@location(0) uv: vec2f,
}

const kMipLevels = 4;
const baseMipSize: u32 = 16;

const kSquare = array(
vec2f(0, 0), vec2f(0, 1), vec2f(1, 0),
vec2f(1, 0), vec2f(0, 1), vec2f(1, 1),
);

@vertex
fn vmain(
@builtin(vertex_index) vertex_index: u32,
) -> Varying {

let uv = kSquare[vertex_index];
let pos = vec4(uv * vec2(2, -2) + vec2(-1, 1), 0.0, 1.0);

return Varying(pos, uv);
}

const kSampleCount = 4;
const kSamplePositions: array<vec2f, kSampleCount> = array(
vec2f(0.375, 0.125),
vec2f(0.875, 0.375),
vec2f(0.125, 0.625),
vec2f(0.625, 0.875),
);

@fragment
fn fmain(vary: Varying) -> @location(0) vec4f {
let dim = textureDimensions(tex);
let dimMax = max(dim.x, dim.y);

let xy = vary.uv * f32(dimMax);
let xyInt = vec2u(xy);
let xyFrac = xy % vec2f(1, 1);

if xyInt.x >= dim.x || xyInt.y >= dim.y {
return vec4f(0, 0, 0, 1);
}

// check if we're close to a sample, and return it
for (var sampleIndex = 0; sampleIndex < kSampleCount; sampleIndex += 1) {
if distance(xyFrac, kSamplePositions[sampleIndex]) < 0.1 {
let val = textureLoad(tex, xyInt, sampleIndex).rgb;
return vec4f(val, 1);
}
}

// if not close to a sample, render background checkerboard
return vec4f(f32(xyInt.x % 4 + 1) / 8, 0, f32(xyInt.y % 4 + 1) / 8, 1);
}
2 changes: 1 addition & 1 deletion sample/reversedZ/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>webgpu-samples: samplerParameters</title>
<title>webgpu-samples: reversedZ</title>
<style>
:root {
color-scheme: light dark;
Expand Down
Loading
Loading