二、WebGPU阶段间变量(inter-stage variables)
在上一篇文章中,我们介绍了一些关于WebGPU的基础知识。在本文中,我们将介绍阶段变量(inter-stage variables)的基础知识。
阶段变量在顶点着色器和片段着色器之间起作用。当顶点着色器输出3个位置时,三角形将栅格化。顶点着色器可以在每个位置输出额外的值,默认情况下,这些值将在3个点之间进行插值。让我们举个小例子。我们将从上一篇文章中的三角形着色器开始。
我们要做的就是改变着色器。
const module = device.createShaderModule({
label: 'our hardcoded rgb triangle shaders',
code: `
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
var color = array<vec4f, 3>(
vec4f(1, 0, 0, 1), // red
vec4f(0, 1, 0, 1), // green
vec4f(0, 0, 1, 1), // blue
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
vsOutput.color = color[vertexIndex];
return vsOutput;
}
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return fsInput.color;
}
`,
});
首先,我们声明一个结构体。这是一个在顶点着色器和片段着色器之间协调阶段间变量的简单方法。
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
然后我们声明顶点着色器来返回这种类型的结构
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
我们创建一个包含3种颜色的数组。
var color = array<vec4f, 3>(
vec4f(1, 0, 0, 1), // red
vec4f(0, 1, 0, 1), // green
vec4f(0, 0, 1, 1), // blue
);
然后不是返回一个vec4f来获取位置,而是我们声明一个结构的实例,填充它,然后返回它
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
vsOutput.color = color[vertexIndex];
return vsOutput;
在片段着色器中,我们声明它将这些结构之一作为函数的参数
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return fsInput.color;
}
然后返回颜色。
如果我们运行它,我们会看到,每次GPU调用我们的片段着色器时,它都会传递在所有3个点之间插值的颜色。
以下为当前代码及运行结果:
HTML:
<!--
* @Description:
* @Author: tianyw
* @Date: 2022-11-11 12:50:23
* @LastEditTime: 2023-09-17 16:33:32
* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>001hello-triangle</title>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
background: #000;
color: #fff;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
}
div,
canvas {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="003color-triangle">
<canvas id="gpucanvas"></canvas>
</div>
<script type="module" src="./003color-triangle.ts"></script>
</body>
</html>
TS:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-04-08 20:03:35
* @LastEditTime: 2023-09-17 21:06:44
* @LastEditors: tianyw
*/
export type SampleInit = (params: {
canvas: HTMLCanvasElement;
}) => void | Promise<void>;
import shaderWGSL from "./shaders/shader.wgsl?raw";
const init: SampleInit = async ({ canvas }) => {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return;
const device = await adapter?.requestDevice();
if (!device) {
console.error("need a browser that supports WebGPU");
return;
}
const context = canvas.getContext("webgpu");
if (!context) return;
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: "premultiplied"
});
const shaderModule = device.createShaderModule({
label: "our hardcoded rgb triangle shaders",
code: shaderWGSL
});
const renderPipeline = device.createRenderPipeline({
label: "hardcoded rgb triangle pipeline",
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vs"
},
fragment: {
module:shaderModule,
entryPoint: "fs",
targets: [
{
format: presentationFormat
}
]
},
primitive: {
// topology: "line-list"
// topology: "line-strip"
// topology: "point-list"
topology: "triangle-list"
// topology: "triangle-strip"
}
});
function frame() {
const renderCommandEncoder = device.createCommandEncoder({
label: "render vert frag"
});
if (!context) return;
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: "clear",
storeOp: "store"
}
]
};
const renderPass =
renderCommandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3, 1, 0, 0);
renderPass.end();
const renderBuffer = renderCommandEncoder.finish();
device.queue.submit([renderBuffer]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
};
const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader:
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f
}
@vertex
fn vs(@builtin(vertex_index) vertexIndex: u32) -> OurVertexShaderOutput {
let pos = array<vec2f, 3>(
vec2f(0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f(0.5, -0.5) // bottom right
);
var color = array<vec4f,3>(
vec4f(1, 0, 0, 1), // red
vec4f(0, 1, 0, 1), // green
vec4f(0, 0, 1, 1) // blue
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
vsOutput.color = color[vertexIndex];
return vsOutput;
}
@fragment
fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return fsInput.color;
}
阶段间变量最常用于跨三角形插值纹理坐标,我们将在纹理文章中介绍。另一个常见的用法是插值法线穿过三角形,这将在第一篇文章中介绍照明。
阶段变量按位置连接
重要的一点是,就像WebGPU中几乎所有的东西一样,顶点着色器和片段着色器之间的连接是通过索引的。对于阶段间变量,它们通过位置索引连接。
为了了解我的意思,让我们只更改片段着色器,在location(0)处采用vec4f参数,而不是结构体。
@fragment fn fs(@location(0) color: vec4f) -> @location(0) vec4f {
return color;
}
下面两个片段着色器的代码是同等效果的,依然可以渲染出渐变色的三角形。
@builtin(position)
我们的原始着色器在顶点和片段着色器中使用相同的结构,有一个名为position的字段,但它没有位置。它被声明为@builtin(position)。
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
该字段不是阶段间变量。相反,它是内置的。碰巧@builtin(position)在顶点着色器和片段着色器中有不同的含义。
在顶点着色器中,@builtin(position)是GPU在片段着色器中绘制三角形/线/点所需的输出。
在片段着色器中,@builtin(position)是一个输入,是片段着色器当前被要求计算颜色的像素的像素坐标。
像素坐标由像素的边缘指定。提供给片段着色器的值是像素中心的坐标。
如果我们要绘制的纹理大小为3x2像素,这些就是坐标。
我们可以改变我们的着色器来使用这个位置。例如,让我们画一个棋盘。
const module = device.createShaderModule({
label: 'our hardcoded checkerboard triangle shaders',
code: `
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
return vsOutput;
}
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
let red = vec4f(1, 0, 0, 1);
let cyan = vec4f(0, 1, 1, 1);
let grid = vec2u(fsInput.position.xy) / 8;
let checker = (grid.x + grid.y) % 2 == 1;
return select(red, cyan, checker);
}
`,
});
position 被声明为@builtin(position),它会将xy坐标转换为vec2u,后者是两个无符号整数。然后将它们除以8,得到每8个像素增加一次的计数。然后,它将x和y网格坐标相加,计算模块2,并将结果与模块1进行比较。这将给我们一个布尔值,每隔一个整数就为true或false。最后,它使用WGSL函数select 给定2个值,根据布尔条件选择其中一个。在JavaScript中,select是这样写的
// If condition is false return `a`, otherwise return `b`
select = (a, b, condition) => condition ? b : a;
代码及运行结果:
HTML:
<!--
* @Description:
* @Author: tianyw
* @Date: 2022-11-11 12:50:23
* @LastEditTime: 2023-09-17 16:33:32
* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>001hello-triangle</title>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
background: #000;
color: #fff;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
}
div,
canvas {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="004color-grid-triangle">
<canvas id="gpucanvas"></canvas>
</div>
<script type="module" src="./004color-grid-triangle.ts"></script>
</body>
</html>
TS:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-04-08 20:03:35
* @LastEditTime: 2023-09-17 21:06:44
* @LastEditors: tianyw
*/
export type SampleInit = (params: {
canvas: HTMLCanvasElement;
}) => void | Promise<void>;
import shaderWGSL from "./shaders/shader.wgsl?raw";
const init: SampleInit = async ({ canvas }) => {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return;
const device = await adapter?.requestDevice();
if (!device) {
console.error("need a browser that supports WebGPU");
return;
}
const context = canvas.getContext("webgpu");
if (!context) return;
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: "premultiplied"
});
const shaderModule = device.createShaderModule({
label: "our hardcoded rgb triangle shaders",
code: shaderWGSL
});
const renderPipeline = device.createRenderPipeline({
label: "hardcoded rgb triangle pipeline",
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vs"
},
fragment: {
module:shaderModule,
entryPoint: "fs",
targets: [
{
format: presentationFormat
}
]
},
primitive: {
// topology: "line-list"
// topology: "line-strip"
// topology: "point-list"
topology: "triangle-list"
// topology: "triangle-strip"
}
});
function frame() {
const renderCommandEncoder = device.createCommandEncoder({
label: "render vert frag"
});
if (!context) return;
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: "clear",
storeOp: "store"
}
]
};
const renderPass =
renderCommandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3, 1, 0, 0);
renderPass.end();
const renderBuffer = renderCommandEncoder.finish();
device.queue.submit([renderBuffer]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
};
const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader:
struct OurVertexShaderOutput {
@builtin(position) position: vec4f
}
@vertex
fn vs(@builtin(vertex_index) vertexIndex: u32) -> OurVertexShaderOutput {
let pos = array<vec2f, 3>(
vec2f(0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f(0.5, -0.5) // bottom right
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
return vsOutput;
}
@fragment
fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
let red = vec4f(1,0,0,1);
let cyan = vec4f(0, 1, 1, 1);
let grid = vec2u(fsInput.position.xy) / 8;
let checker = (grid.x + grid.y) % 2 == 1;
return select(red, cyan, checker);
}
即使你在片段着色器中不使用@builtin(position),它的存在也很方便,因为它意味着我们可以在顶点着色器和片段着色器中使用同一个结构体。需要注意的是,顶点着色器和片段着色器中的position结构体字段是完全不相关的。它们是完全不同的变量。
如上所述,对于阶段间变量,重要的是@location(?)。因此,为顶点着色器的输出和片段着色器的输入声明不同的结构体是很常见的。
为了让这个更清楚,在我们的例子中,顶点着色器和片段着色器在同一个字符串中只是为了方便。我们也可以将它们分成单独的模块。
const vsModule = device.createShaderModule({
label: 'hardcoded triangle',
code: `
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
return vsOutput;
}
`,
});
const fsModule = device.createShaderModule({
label: 'checkerboard',
code: `
@fragment fn fs(@builtin(position) pixelPosition: vec4f) -> @location(0) vec4f {
let red = vec4f(1, 0, 0, 1);
let cyan = vec4f(0, 1, 1, 1);
let grid = vec2u(pixelPosition.xy) / 8;
let checker = (grid.x + grid.y) % 2 == 1;
return select(red, cyan, checker);
}
`,
});
我们必须更新创建的管道才能使用它们
const pipeline = device.createRenderPipeline({
label: 'hardcoded checkerboard triangle pipeline',
layout: 'auto',
vertex: {
module: vsModule,
entryPoint: 'vs',
},
fragment: {
module: fsModule,
entryPoint: 'fs',
targets: [{ format: presentationFormat }],
},
});
这里 demo 只更改了 fragment 的代码,效果等同:
关键是,在大多数WebGPU示例中,两个着色器使用相同的字符串只是为了方便。实际上,首先WebGPU解析WGSL以确保其语法正确。然后,WebGPU查看你指定的入口点。从那里开始,它会查看入口点引用的部分,而不是该入口点的其他部分。它很有用,因为如果两个或多个着色器共享绑定、结构、常量或函数,就不需要两次输入结构、绑定和分组位置等内容。但是,从WebGPU的角度来看,就好像您为每个入口点复制了所有它们一样。
注意:使用@builtin(position)生成棋盘并不常见。棋盘或其他图案更常用纹理来实现。实际上,如果调整窗口的大小,就会出现问题。因为棋盘是基于画布的像素坐标,所以它是相对于画布的,而不是相对于三角形的。
插值设置
我们在上面看到,阶段间变量,顶点着色器的输出,在传递给片段着色器时进行插值。有两组设置可以改变插值的发生方式。将它们设置为默认值以外的任何值并不常见,但有一些用例将在其他文章中介绍。
插值类型:
- perspective:值以正确的透视方式(默认)插值。
- linear:值以线性的、非透视的正确方式插值。
- falt:值不进行插值。插值采样不用于平面插值
插值采样(Interpolation sampling):
- center:在像素的中心执行插值(默认)
- centroid:在当前基元内的碎片覆盖的所有样本内的一点执行插值。这个值对于原始类型中的所有样本都是相同的。
- sample:对每个样本进行插值。应用这个属性时,每个样本都会调用一次片段着色器。
将它们指定为属性。例如:
@location(2) @interpolate(linear, center) myVariableFoo: vec4f;
@location(3) @interpolate(flat) myVariableBar: vec4f;
请注意,如果阶段间变量是整数类型,则必须将其插值设置为平坦 flat。
如果将插值类型设置为flat,则传递给片段着色器的值就是该三角形中第一个顶点的 变量的值。
在下一篇文章中,我们将介绍uniform作为传递数据到着色器的另一种方法。