Skip to content

Commit

Permalink
add centroid info
Browse files Browse the repository at this point in the history
  • Loading branch information
greggman committed Jan 11, 2024
1 parent 8d1775f commit 0f54419
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 42 deletions.
222 changes: 186 additions & 36 deletions webgpu/lessons/webgpu-multisampling.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,59 +201,209 @@ see the one on the right has been antialiased.
Some things to note:
* `count` must be `4`
## `count` must be `4`
In WebGPU version 1, you can only set `multisample: { count }` on a render pipeline
to 4 or 1. Similarly you can only set the `sampleCount` on a texture to 4 or 1.
1 is the default and means the texture is not multisampled.
In WebGPU version 1, you can only set `multisample: { count }` on a render pipeline
to 4 or 1. Similarly you can only set the `sampleCount` on a texture to 4 or 1.
1 is the default and means the texture is not multisampled.
* <a id="a-not-a-grid"></a> Multisampling does not use a grid
## <a id="a-not-a-grid"></a> Multisampling does not use a grid
As pointed out above, multisampling does not happen on a grid. For sampleCount = 4
the sample locations are like this.
As pointed out above, multisampling does not happen on a grid. For sampleCount = 4
the sample locations are like this.
<img src="resources/multisample-4x.svg" width="256" >
count: 2
<div class="webgpu_center">
<img src="resources/multisample-4x.svg" width="256">
<div class="center">count: 4</div>
</div>
<div class="webgpu_center">
<img src="resources/multisample-2x.svg" width="256">
<div class="center">count: 2</div>
</div>
count: 8
<div class="webgpu_center">
<img src="resources/multisample-8x.svg" width="256">
<div class="center">count: 8</div>
</div>
count: 16
<div class="webgpu_center">
<img src="resources/multisample-16x.svg" width="256">
<div class="center">count: 16</div>
</div>
**WebGPU currently only supports a count of 4**
## You do not have to set a resolve target on every render pass
Setting `colorAttachment[0].resolveTarget` says to WebGPU, "when all the drawing in this render pass has finished,
downscale the multisample texture into the texture set on `resolveTarget`. If you have multiple
render passes you probably don't want to resolve until the last pass. While it's fastest to
resolve in the last pass it's also perfectly acceptable
to make an empty last render pass to do nothing but resolve.
Just make sure you set the `loadOp` to `'load'`
and not `'clear'` in all the passes except the first pass otherwise it will be cleared.
## You can optionally run the fragment shader on each sample point.
Above we said that the fragment shader only runs once for every 4 samples in the multisample
texture. It runs it once and then it stores the result in the samples that were actually inside
the triangle. This is why it's faster than rendering at 4x the resolution.
In [the article on inter-stage variables](webgpu-inter-stage-variables.html#a-interpolate)
we brought up that you can mark how to interpolate inter-stage variables
with the `@interpolate(...)` attribute. One option
is `sample`, in which case the fragment shader will be run once for each sample.
There are also builtins like `@builtin(sample_index)`, which well tell you which sample
you are currently working on, and `@builtin(sample_mask)`, which, as an input, will tell you which
samples were inside the triangle, and, as an output, will they you prevent sample points
from getting updated.
## `center` vs `centroid`
There are 3 *sampling* interpolation modes. Above we mentioned `'sample'` mode
where the fragment shader is called once for each sample. The other two modes are
`'center'`, which is the default, and `'centroid'`.
* `'center'` interpolates values relative to the center of the pixel.
<div class="webgpu_center">
<img src="resources/multisample-centroid-issue.svg" width="400">
</div>
Above we can see a single pixel/texel where
sample points `s1` and `s3` are inside the triangle. Our fragment shader will be called one
time and it will be passed inter-stage variables with their values interpolated relative
to the center (`c`) of the pixel. The problem is, **`c` is outside of the triangle**.
This might not matter, but it's possible you have some math that assumes the value is inside
the triangle. I don't know of a good example but imagine we add barycentric coordinates, one
at each point. Barycentric coordinate are basically 3 coordinates that go from zero to
one where each value represents how far from one of the vertices of the triangle a specific
position is. To do this, we just add barycentric points like this.
```wgsl
+struct VOut {
+ @builtin(position) position: vec4f,
+ @location(0) baryCoord: vec3f,
+};

@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
-) -> @builtin(position) vec4f {
+) -> VOut {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
+ let bary = array(
+ vec3f(1, 0, 0),
+ vec3f(0, 1, 0),
+ vec3f(0, 0, 1),
+ );
- return vec4f(pos[vertexIndex], 0.0, 1.0);
+ var vout: VOut;
+ vout.position = vec4f(pos[vertexIndex], 0.0, 1.0);
+ vout.baryCoord = bary[vertexIndex];
+ return vout;
}

-@fragment fn fs() -> @location(0) vec4f {
- return vec4f(1, 0, 0, 1);
+@fragment fn fs(vin: VOut) -> @location(0) vec4f {
+ let allAbove0 = all(vin.baryCoord >= vec3f(0));
+ let allBelow1 = all(vin.baryCoord <= vec3f(1));
+ let inside = allAbove0 && allBelow1;
+ let red = vec4f(1, 0, 0, 1);
+ let yellow = vec4f(1, 1, 0, 1);
+ return select(yellow, red, inside);
}
```
Above we're associating `1, 0, 0` with the first point, `0, 1, 0` with the 2nd,
and `0, 0, 1` with the 3rd. Interpolating between them, no value should be below
0 or above 1.
In the fragment shader we test if all three (x, y, and z) of those interpolated values are `>= 0` with
`all(vin.baryCoord >= vec3f(0))`. We also test if they are all `<= 1` with
`all(vin.baryCoord <= vec3f(1))`. Finally we `&` the 2 together. This tells
us if we're inside or outside the triangle. The end selects red if we're inside
and yellow if not inside. Since we're interpolating *between* the vertices
you'd expect them to always be inside.
To try it out let also make our example be lower resolution so it's easier
to see the results
```js
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;
+ const width = entry.contentBoxSize[0].inlineSize / 16 | 0;
+ const height = entry.contentBoxSize[0].blockSize / 16 | 0;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
// re-render
render();
}
});
observer.observe(canvas);
```
and some CSS
**WebGPU currently only supports a count of 4**
```js
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
```
Which if we run we see this
* You can optionally run the fragment shader on each sample point.
{{{example url="../webgpu-multisample-center-issue.html"}}}
Above we said that the fragment shader only runs once for every 4 samples in the multisample
texture. It runs it once and then it stores the result in the samples that were actually inside
the triangle. This is why it's faster than rendering at 4x the resolution.
We can see, some of the edge pixels have yellow in them. This is because, as
pointed out above, the interpolated inter-stage variable values that are
passed to the fragment shader are relative to the center of the pixel.
That center is outside the triangle in the cases where we're seeing
yellow.
Switching the interpolation sample mode to `'centroid'` tries to fix this
issue. In `'centroid'` mode, the GPU uses the centroid of the area of the triangle
that's inside the pixel.
<div class="webgpu_center">
<img src="resources/multisample-centroid-fix.svg" width="400">
</div>
If we take our sample and change the interpolation mode to `'centroid'`
```wgsl
struct VOut {
@builtin(position) position: vec4f,
- @location(0) baryCoord: vec3f,
+ @location(0) @interpolate(perspective, centroid) baryCoord: vec3f,
};
```
In [the article on inter-stage variables](webgpu-inter-stage-variables.html#a-interpolate)
we brought up that you can mark how to interpolate inter-stage variables
with the `@interpolate(...)` attribute. One option
is `sample`, in which case the fragment shader will be run once for each sample.
There are also builtins like `@builtin(sample_index)`, which well tell you which sample
you are currently working on, and `@builtin(sample_mask)`, which, as an input, will tell you which
samples were inside the triangle, and, as an output, will they you prevent sample points
from getting updated.
Now the GPU passes the inter-stage variables interpolated values relative
to the centroid and the issue of the yellow pixels goes away.
* You do not have to set a resolve target on every render pass
{{{example url="../webgpu-multisample-centroid.html"}}}
Setting `colorAttachment[0].resolveTarget` says to WebGPU, "when all the drawing in this render pass has finished,
downscale the multisample texture into the texture set on `resolveTarget`. If you have multiple
render passes you probably don't want to resolve until the last pass. While it's fastest to
resolve in the last pass it's also perfectly acceptable
to make an empty last render pass to do nothing but resolve.
Just make sure you set the `loadOp` to `'load'`
and not `'clear'` in all the passes except the first pass otherwise it will be cleared.
> Note: The GPU may or may not actually compute the actual centroid
of the area of the triangle inside the pixel. All the guarantees
is that the inter-stage variables will be interpolated relative to
some area inside the part of the triangle that intersects the pixel.
## What about inside a triangle?
## What about anti-aliasing inside a triangle?
Multisampling generally only helps the edges of triangles. Since it's only calling the fragment
shader once, when all sample positions are inside the triangle we just get the same result of the fragment shader
Expand Down
8 changes: 6 additions & 2 deletions webgpu/webgpu-multisample-center-issue.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.0, user-scalable=yes">
<title>WebGPU Simple Triangle with Multisampling</title>
<title>WebGPU Multisampling - center issue</title>
<style>
@import url(resources/webgpu-lesson.css);
html, body {
Expand All @@ -12,6 +12,7 @@
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
Expand Down Expand Up @@ -46,6 +47,7 @@
@builtin(position) position: vec4f,
@location(0) baryCoord: vec3f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> VOut {
Expand All @@ -66,7 +68,9 @@
}
@fragment fn fs(vin: VOut) -> @location(0) vec4f {
let inside = all(vin.baryCoord >= vec3f(0)) && all(vin.baryCoord <= vec3f(1));
let allAbove0 = all(vin.baryCoord >= vec3f(0));
let allBelow1 = all(vin.baryCoord <= vec3f(1));
let inside = allAbove0 && allBelow1;
let red = vec4f(1, 0, 0, 1);
let yellow = vec4f(1, 1, 0, 1);
return select(yellow, red, inside);
Expand Down
11 changes: 7 additions & 4 deletions webgpu/webgpu-multisample-centroid.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.0, user-scalable=yes">
<title>WebGPU Simple Triangle with Multisampling</title>
<title>WebGPU Multisampling - centroid sampling</title>
<style>
@import url(resources/webgpu-lesson.css);
html, body {
Expand All @@ -12,6 +12,7 @@
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
Expand Down Expand Up @@ -44,9 +45,9 @@
code: `
struct VOut {
@builtin(position) position: vec4f,
@location(0) @interpolate(perspective, centroid)
baryCoord: vec3f,
@location(0) @interpolate(perspective, centroid) baryCoord: vec3f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> VOut {
Expand All @@ -67,7 +68,9 @@
}
@fragment fn fs(vin: VOut) -> @location(0) vec4f {
let inside = all(vin.baryCoord >= vec3f(0)) && all(vin.baryCoord <= vec3f(1));
let allAbove0 = all(vin.baryCoord >= vec3f(0));
let allBelow1 = all(vin.baryCoord <= vec3f(1));
let inside = allAbove0 && allBelow1;
let red = vec4f(1, 0, 0, 1);
let yellow = vec4f(1, 1, 0, 1);
return select(yellow, red, inside);
Expand Down

0 comments on commit 0f54419

Please sign in to comment.