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

alphaToCoverage: show resolve result; draw twice with different colors/alphas #445

Merged
merged 7 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
163 changes: 116 additions & 47 deletions sample/alphaToCoverage/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@ quitIfWebGPUNotAvailable(adapter, device);
// GUI controls
//

const kAlphaSteps = 64;

const kInitConfig = {
width: 8,
height: 8,
alpha: 4,
sizeLog2: 3,
showResolvedColor: true,
color1: 0x0000ff,
alpha1: 127,
color2: 0xff0000,
alpha2: 16,
pause: false,
};
const config = { ...kInitConfig };
const updateConfig = () => {
const data = new Float32Array([config.alpha / kAlphaSteps]);
device.queue.writeBuffer(bufConfig, 0, data);
};

const gui = new GUI();
gui.width = 300;
{
const buttons = {
initial() {
Expand All @@ -38,15 +36,19 @@ const gui = new GUI();

const settings = gui.addFolder('Settings');
settings.open();
settings.add(config, 'width', 1, 16, 1);
settings.add(config, 'height', 1, 16, 1);
settings.add(config, 'sizeLog2', 0, 8, 1).name('size = 2**');
settings.add(config, 'showResolvedColor', true);

const alphaPanel = gui.addFolder('Alpha');
alphaPanel.open();
alphaPanel
.add(config, 'alpha', -2, kAlphaSteps + 2, 1)
.name(`alpha (of ${kAlphaSteps})`);
alphaPanel.add(config, 'pause', false);
const draw1Panel = gui.addFolder('Draw 1');
draw1Panel.open();
draw1Panel.addColor(config, 'color1').name('color');
draw1Panel.add(config, 'alpha1', 0, 255).name('alpha');

const draw2Panel = gui.addFolder('Draw 2');
draw2Panel.open();
draw2Panel.addColor(config, 'color2').name('color');
draw2Panel.add(config, 'alpha2', 0, 255).name('alpha');
draw2Panel.add(config, 'pause', false);

gui.add(buttons, 'initial').name('reset to initial');
}
Expand All @@ -71,14 +73,66 @@ context.configure({
});

//
// Config buffer
// GPU state controlled by the config gui
//

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

let multisampleTexture: GPUTexture, multisampleTextureView: GPUTextureView;
let resolveTexture: GPUTexture, resolveTextureView: GPUTextureView;
let lastSize = 0;
function resetMultisampleTexture() {
const size = 2 ** config.sizeLog2;
if (lastSize !== size) {
if (multisampleTexture) {
multisampleTexture.destroy();
}
multisampleTexture = device.createTexture({
format: 'rgba8unorm',
usage:
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
size: [size, size],
sampleCount: 4,
});
multisampleTextureView = multisampleTexture.createView();

if (resolveTexture) {
resolveTexture.destroy();
}
resolveTexture = device.createTexture({
format: 'rgba8unorm',
usage:
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
size: [size, size],
});
resolveTextureView = resolveTexture.createView();

lastSize = size;
}
}

function applyConfig() {
// Update the colors in the (instance-step-mode) vertex buffer
const data = new Uint8Array([
// instance 0 color
(config.color1 >> 16) & 0xff, // R
(config.color1 >> 8) & 0xff, // G
(config.color1 >> 0) & 0xff, // B
config.alpha1,
// instance 1 color
(config.color2 >> 16) & 0xff, // R
(config.color2 >> 8) & 0xff, // G
(config.color2 >> 0) & 0xff, // B
config.alpha2,
]);
device.queue.writeBuffer(bufInstanceColors, 0, data);

resetMultisampleTexture();
}

//
// Pipeline to render to a multisampled texture using alpha-to-coverage
//
Expand All @@ -89,21 +143,23 @@ const renderWithAlphaToCoverageModule = device.createShaderModule({
const renderWithAlphaToCoveragePipeline = device.createRenderPipeline({
label: 'renderWithAlphaToCoveragePipeline',
layout: 'auto',
vertex: { module: renderWithAlphaToCoverageModule },
vertex: {
module: renderWithAlphaToCoverageModule,
buffers: [
{
stepMode: 'instance',
arrayStride: 4,
attributes: [{ shaderLocation: 0, format: 'unorm8x4', offset: 0 }],
},
],
},
fragment: {
module: renderWithAlphaToCoverageModule,
targets: [{ format: 'rgba16float' }],
targets: [{ format: 'rgba8unorm' }],
},
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
Expand All @@ -126,44 +182,56 @@ 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();
applyConfig();

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

const commandEncoder = device.createCommandEncoder();
// clear resolveTextureView to gray if it won't be used
if (!config.showResolvedColor) {
const pass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: resolveTextureView,
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.end();
}
// renderWithAlphaToCoverage pass
{
const pass = commandEncoder.beginRenderPass({
label: 'renderWithAlphaToCoverage pass',
colorAttachments: [
{
view: multisampleTextureView,
clearValue: [0, 0, 0, 1], // will be overwritten
resolveTarget: config.showResolvedColor
? resolveTextureView
: undefined,
clearValue: [0, 0, 0, 1], // black background
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(renderWithAlphaToCoveragePipeline);
pass.setBindGroup(0, renderWithAlphaToCoverageBG);
pass.draw(6);
pass.setVertexBuffer(0, bufInstanceColors);
pass.draw(6, 2);
pass.end();
}
// showMultisampleTexture pass
{
const pass = commandEncoder.beginRenderPass({
label: 'showMulitsampleTexture pass',
label: 'showMultisampleTexture pass',
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
Expand All @@ -179,13 +247,14 @@ function render() {
pass.end();
}
device.queue.submit([commandEncoder.finish()]);

multisampleTexture.destroy();
}

function frame() {
if (!config.pause) {
config.alpha = ((performance.now() / 10000) % 1) * (kAlphaSteps + 4) - 2;
// scrub alpha2 over 15 seconds
let alpha = ((performance.now() / 15000) % 1) * (255 + 20) - 10;
alpha = Math.max(0, Math.min(alpha, 255));
config.alpha2 = alpha;
gui.updateDisplay();
}
updateCanvasSize();
Expand Down
18 changes: 16 additions & 2 deletions sample/alphaToCoverage/meta.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
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.',
description: `
Alpha-to-coverage is an alternative to alpha testing and alpha blending. See:
<https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f>

This sample visualizes how alpha-to-coverage translates alpha values into sample
coverage on your device. It draws two full-screen quads into a 4-sample
texture, each with the configured color and alpha value. Then, it visualizes the
contents of the resulting 4-sample texture: the circles show the 4 samples of
each texel; the background shows the "resolved" results (average of 4 samples).

The algorithm that converts alpha to a coverage sample mask varies per device.
This results in different average "blending" proportions between the black
background, the first draw, and the second draw.
Device differences include different tile sizes (e.g. 1x1, 2x2, or 4x4),
"moving" samples (or not) around with in the tile as alpha increases, etc.
`,
filename: __DIRNAME__,
sources: [
{ path: 'main.ts' },
Expand Down
12 changes: 5 additions & 7 deletions sample/alphaToCoverage/renderWithAlphaToCoverage.wgsl
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
struct Config {
alpha: f32,
};
@group(0) @binding(0) var<uniform> config: Config;

struct Varying {
@builtin(position) pos: vec4f,
// Color from instance-step-mode vertex buffer
@location(0) color: vec4f,
}

@vertex
fn vmain(
@builtin(vertex_index) vertex_index: u32,
@location(0) color: vec4f,
) -> 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));
return Varying(vec4(square[vertex_index], 0, 1), color);
}

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

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

struct Varying {
@builtin(position) pos: vec4f,
Expand All @@ -24,6 +25,7 @@ fn vmain(
return Varying(pos, uv);
}

// Standard sample positions for 4xMSAA (assuming the device conforms to spec)
const kSampleCount = 4;
const kSamplePositions: array<vec2f, kSampleCount> = array(
vec2f(0.375, 0.125),
Expand All @@ -32,6 +34,12 @@ const kSamplePositions: array<vec2f, kSampleCount> = array(
vec2f(0.625, 0.875),
);

// Compute dimensions for drawing a nice-looking visualization
const kSampleDistanceFromCloseEdge = 0.125; // from the standard sample positions
const kGridEdgeHalfWidth = 0.025;
const kSampleInnerRadius = kSampleDistanceFromCloseEdge - kGridEdgeHalfWidth;
const kSampleOuterRadius = kSampleDistanceFromCloseEdge + kGridEdgeHalfWidth;

@fragment
fn fmain(vary: Varying) -> @location(0) vec4f {
let dim = textureDimensions(tex);
Expand All @@ -41,18 +49,29 @@ fn fmain(vary: Varying) -> @location(0) vec4f {
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);
}
// Show the visualization only if the resolution is large enough to see it
if (dpdx(xy.x) < kGridEdgeHalfWidth * 2) & (dpdy(xy.y) < kGridEdgeHalfWidth * 2) {
// Check if we're close to a sample; if so, visualize the sample value
for (var sampleIndex = 0; sampleIndex < kSampleCount; sampleIndex += 1) {
let distanceFromSample = distance(xyFrac, kSamplePositions[sampleIndex]);
if distanceFromSample < kSampleInnerRadius {
// Draw a circle for the sample value
let val = textureLoad(tex, xyInt, sampleIndex).rgb;
return vec4f(val, 1);
} else if distanceFromSample < kSampleOuterRadius {
// Draw a ring around the circle
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 close to a grid edge, render the grid
let distanceToGridEdge = abs((xyFrac + 0.5) % 1 - 0.5);
if min(distanceToGridEdge.x, distanceToGridEdge.y) < kGridEdgeHalfWidth {
return vec4f(0, 0, 0, 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);
// Otherwise, show the multisample-resolved result as the background
let val = textureLoad(resolved, xyInt, /*level*/ 0).rgb;
return vec4f(val, 1);
}
Loading