Wgpu The Pipeline

What’s a pipeline?

If you’re familiar with OpenGL, you may remember using shader programs. You can think of a pipeline as a more robust version of that. A pipeline describes all the actions the gpu will perform when acting on a set of data. In this section, we will be creating a RenderPipeline specifically.

如果您熟悉OpenGL,可能还记得使用着色器程序。您可以将管道视为更健壮的版本。管道描述了gpu在处理一组数据时将执行的所有操作。在本节中,我们将专门创建渲染管线。

Wait shaders?

Shaders are mini programs that you send to the gpu to perform operations on your data. There are 3 main types of shader: vertex, fragment, and compute. There are others such as geometry shaders, but they’re more of an advanced topic. For now we’re just going to use vertex, and fragment shaders.

着色器是发送到gpu以对数据执行操作的小型程序。着色器有3种主要类型:顶点、片段和计算。还有其他一些,如几何体着色器,但它们更像是一个高级主题。现在我们将使用顶点和片段着色器。

Vertex, fragment.. what are those?

A vertex is a point in 3d space (can also be 2d). These vertices are then bundled in groups of 2s to form lines and/or 3s to form triangles.

顶点是三维空间中的一个点(也可以是二维的)。然后将这些顶点捆绑成2个一组,形成直线或3个一组,形成三角形。

vertices

Most modern rendering uses triangles to make all shapes, from simple shapes (such as cubes), to complex ones (such as people). These triangles are stored as vertices which are the points that make up the corners of the triangles.

大多数现代渲染使用三角形生成所有形状,从简单形状(如立方体)到复杂形状(如人)。这些三角形存储为顶点,这些顶点是构成三角形角的点。

We use a vertex shader to manipulate the vertices, in order to transform the shape to look the way we want it.

我们使用顶点着色器来操纵顶点,以便将形状变换为我们想要的样子。

The vertices are then converted into fragments. Every pixel in the result image gets at least one fragment. Each fragment has a color that will be copied to its corresponding pixel. The fragment shader decides what color the fragment will be.

然后将顶点转换为片段。结果图像中的每个像素至少获得一个片段。每个片段都有一个将被复制到其相应像素的颜色。片段着色器决定片段的颜色。

WGSL

WebGPU supports two shader languages natively: SPIR-V, and WGSL. SPIR-V is actually a binary format developed by Kronos to be a compilation target for other languages such as GLSL and HLSL. It allows for easy porting of code. The only problem is that it’s not human readable as it’s a binary language. WGSL is meant to fix that. WGSL’s development focuses on getting it to easily convert into SPIR-V. WGPU even allows us to supply WGSL for our shaders.

WebGPU本机支持两种着色器语言:SPIR-V和WGSL。SPIR-V实际上是Kronos开发的二进制格式,用于其他语言(如GLSL和HLSL)的编译目标。它允许轻松移植代码。唯一的问题是它不是人类可读的,因为它是一种二进制语言。WGSL旨在解决这一问题。WGSL的开发重点是使其能够轻松转换为SPIR-V。WGPU同样允许我们为着色器提供WGSL。

If you’ve gone through this tutorial before you’ll likely notice that I’ve switched from using GLSL to using WGSL. Given that GLSL support is a secondary concern and that WGSL is the first class language of WGPU, I’ve elected to convert all the tutorials to use WGSL. Some of the showcase examples still use GLSL, but the main tutorial and all examples going forward will be using WGSL.

如果您之前阅读过本教程,您可能会注意到我已从使用GLSL切换到使用WGSL。考虑到GLSL支持是第二个问题,而WGSL是WGPU的第一类语言,我选择将所有教程转换为使用WGSL。一些showcase示例仍然使用GLSL,但主教程和所有后续示例将使用WGSL。

The WGSL spec and it’s inclusion in WGPU is still in development. If you run into trouble using it, you may want the folks at https://app.element.io/#/room/#wgpu:matrix.org to take a look at your code.

WGSL规范及其包含在WGPU中的内容仍在开发中。如果你在使用它时遇到麻烦,你可能想让人们在https://app.element.io/#/room/#wgpu:matrix.org 来看看你的代码。

Writing the shaders

In the same folder as main.rs, create a file shader.wgsl. Write the following code in shader.wgsl.

在与main.rs相同的文件夹中,创建文件shader.wgsl。在shader.wgsl中编写以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Vertex shader

struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
};

[[stage(vertex)]]
fn main(
[[builtin(vertex_index)]] in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let x = f32(1 - i32(in_vertex_index)) * 0.5;
let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out;
}

First we declare struct to store the output of our vertex shader. This consists of only one field currently which is our vertex’s clip_position. The [[builtin(position)]] bit tells WGPU that this is the value we want to use as the vertex’s clip coordinates. This is analogous to GLSL’s gl_Position variable.

首先,我们声明struct来存储顶点着色器的输出。这当前只包含一个字段,它是顶点的剪辑位置。[[builtin(position)]位告诉WGPU这是我们要用作顶点剪辑坐标的值。这类似于GLSL的gl_位置变量。

Vector types such as vec4 are generic. Currently you must specify the type of value the vector will contain. Thus a 3D vector using 32bit floats would be vec3.

向量类型(如vec4)是泛型的。当前必须指定向量将包含的值的类型。因此,使用32位浮点的3D向量将是vec3

The next part of the shader code is the main function. We are using [[stage(vertex)]] to mark this function as a valid entry point for a vertex shader. We expect a u32 called in_vertex_index which gets its value from [[builtin(vertex_index)]].

着色器代码的下一部分是主函数。我们使用[[stage(vertex)]]将此函数标记为顶点着色器的有效入口点。我们期望一个名为in_vertex_index的u32,它的值来自[[builtin(vertex_index)]]。

We then declare a variable called out using our VertexOutput struct. We create two other variables for the x, and y, of a triangle.

然后,我们声明一个使用VertexOutput结构调用的变量。我们为三角形创建x和y及另外两个变量。

The f32() and i32() bits are examples of casts.

f32()和i32()位是强制转换的示例。

Variables defined with var can be modified, but must specify their type. Variables created with let can have their types inferred, but their value cannot be changed during the shader.

可以修改用’var’定义的变量,但必须指定其类型。使用’let’创建的变量可以推断其类型,但在着色器期间不能更改其值。

Now we can save our clip_position to out. We then just return out and we’re done with the vertex shader!

现在我们可以将clip_position保存为out。然后我们返回,顶点着色器就完成了!

We technically didn’t need a struct for this example, and could have just done something like the following:

从技术上讲,本例不需要结构,只需执行以下操作即可:

1
2
3
4
5
6
[[stage(vertex)]]
fn main(
[[builtin(vertex_index)]] in_vertex_index: u32
) -> [[builtin(position)]] vec4<f32> {
// Vertex shader code...
}

We’ll be adding more fields to VertexOutput later, so we might as well start using it now.

稍后,我们将向VertexOutput添加更多字段,因此我们不妨现在就开始使用它。

Next up the fragment shader. Still in shader.wgsl add the follow:

接下来是片段着色器。仍在shader.wgsl中添加以下内容:

1
2
3
4
5
6
// Fragment shader

[[stage(fragment)]]
fn main(in: VertexOutput) -> [[location(0)]] vec4<f32> {
return vec4<f32>(0.3, 0.2, 0.1, 1.0);
}

All this does is set the color of the current fragment to brown color.

所有这些操作都是将当前片段的颜色设置为棕色。

Notice that this function is also called main. Because this function is marked as a fragment shader entry point, this is ok. You can change the names around if you like, but I’ve opted to keep them the same.

请注意,此函数也称为main。因为此函数被标记为片段着色器入口点,所以这是确定的。如果你愿意的话,你可以改变名字,但我还是选择了保持不变。

The [[location(0)]] bit tells WGPU to store the value the vec4 returned by this function in the first color target. We’ll get into what this is later.

[[location(0)]]位告诉WGPU将此函数返回的vec4值存储在第一个颜色目标中。我们以后再谈这件事。

How do we use the shaders?

This is the part where we finally make the thing in the title: the pipeline. First let’s modify State to include the following.

接着我们最终制作标题中的东西的部分:pipeline。首先,让我们修改State以包括以下内容。

1
2
3
4
5
6
7
8
9
10
11
// main.rs
struct State {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
size: winit::dpi::PhysicalSize<u32>,
// NEW!
render_pipeline: wgpu::RenderPipeline,
}

Now let’s move to the new() method, and start making the pipeline. We’ll have to load in those shaders we made earlier, as the render_pipeline requires those.

现在让我们转到new()方法,开始制作pipeline。我们必须加载之前制作的着色器,因为render_pipeline需要这些着色器。

1
2
3
4
5
let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
flags: wgpu::ShaderFlags::all(),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});

One more thing, we need to create a PipelineLayout. We’ll get more into this after we cover Buffers.

还有一件事,我们需要创建一个PipelineLayout。在我们讨论缓冲区之后,我们将进一步讨论这个问题。

1
2
3
4
5
6
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});

Finally we have all we need to create the render_pipeline.

最后,我们有了创建render_pipeline所需的一切。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "main", // 1.
buffers: &[], // 2.
},
fragment: Some(wgpu::FragmentState { // 3.
module: &shader,
entry_point: "main",
targets: &[wgpu::ColorTargetState { // 4.
format: sc_desc.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrite::ALL,
}],
}),
// continued ...

Two things to note here:
这里需要注意两件事:

  1. Here you can specify which function inside of the shader should be called, which is known as the entry_point. These are the functions we marked with [[stage(vertex)]] and [[stage(fragment)]]
  2. The buffers field tells wgpu what type of vertices we want to pass to the vertex shader. We’re specifying the vertices in the vertex shader itself so we’ll leave this empty. We’ll put something there in the next tutorial.
  3. The fragment is technically optional, so you have to wrap it in Some(). We need it if we want to store color data to the swap_chain.
  4. The targets field tells wgpu what color outputs it should set up.Currently we only need one for the swap_chain. We use the swap_chain’s format so that copying to it is easy, and we specify that the blending should just replace old pixel data with new data. We also tell wgpu to write to all colors: red, blue, green, and alpha. We’ll talk more aboutcolor_state when we talk about textures.
  1. 在这里,您可以指定应该调用着色器内部的哪个函数,该函数称为入口点。这些是我们用[[stage(vertex)]]和[[stage(fragment)]标记的函数
  2. buffers字段告诉wgpu要传递给顶点着色器的顶点类型。我们将在顶点着色器本身中指定顶点,因此将此项留空。我们将在下一个教程中介绍一些内容。
  3. 片段着色器在技术上是可选的,因此您必须将其包装在Some()中。如果我们想将颜色数据存储到交换链,就需要它。
  4. targets字段告诉wgpu应该设置什么颜色输出。目前,我们只需要一个用于交换链。我们使用swap_chain的格式,以便复制到它是很容易的,并且我们指定混合应该只是用新数据替换旧的像素数据。我们还告诉wgpu写入所有颜色:红色、蓝色、绿色和alpha。在讨论纹理时,我们将更多地讨论颜色状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList, // 1.
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw, // 2.
cull_mode: Some(wgpu::Face::Back),
// Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
polygon_mode: wgpu::PolygonMode::Fill,
// Requires Features::DEPTH_CLAMPING
clamp_depth: false,
// Requires Features::CONSERVATIVE_RASTERIZATION
conservative: false,
},
// continued ...

The primitive field describes how to interpret our vertices when converting them into triangles.
primitive字段描述了在将顶点转换为三角形时如何解释顶点。

  1. Using PrimitiveTopology::TriangleList means that each three vertices will correspond to one triangle.
  2. The front_face and cull_mode fields tell wgpu how to determine whether a given triangle is facing forward or not. FrontFace::Ccw means that a triangle is facing forward if the vertices are arranged in a counter clockwise direction. Triangles that are not considered facing forward are culled (not included in the render) as specified by CullMode::Back. We’ll cover culling a bit more when we cover Buffers.
  1. 使用PrimitiveTopology::TriangleList意味着每三个顶点对应一个三角形。
  2. front_face和cull_mode字段告诉wgpu如何确定给定三角形是否朝前。FrontFace::Ccw表示如果顶点按逆时针方向排列,则三角形朝前。根据CullMode::Back的指定,将剔除(不包括在渲染中)不被视为朝前的三角形。当我们讨论缓冲区时,我们将更多地讨论剔除。
1
2
3
4
5
6
7
    depth_stencil: None, // 1.
multisample: wgpu::MultisampleState {
count: 1, // 2.
mask: !0, // 3.
alpha_to_coverage_enabled: false, // 4.
},
});

The rest of the method is pretty simple:
该方法的其余部分非常简单:

  1. We’re not using a depth/stencil buffer currently, so we leave depth_stencil as None. This will change later.
  2. This determines how many samples this pipeline will use. Multisampling is a complex topic, so we won’t get into it here.
  3. sample_mask specifies which samples should be active. In this case we are using all of them.
  4. alpha_to_coverage_enabled has to do with anti-aliasing. We’re not covering anti-aliasing here, so we’ll leave this as false now.
  1. 我们目前没有使用depth/stencil缓冲区,因此我们将depth/stencil保留为无。这将在以后改变。
  2. 这将确定此pipeline将使用多少个样本。多重采样是一个复杂的主题,因此我们在这里不进行讨论。
  3. sample_mask指定哪些样本应处于活动状态。在本例中,我们使用了所有这些。
  4. 启用alpha_to_coverage_enabled与抗锯齿有关。我们这里不讨论反走样,所以现在将其保留为false。

Now all we have to do is save the render_pipeline to State and then we can use it!

现在我们所要做的就是将render_pipeline保存到State,然后我们就可以使用它了!

1
2
3
4
5
6
7
8
9
10
11
// new()
Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
// NEW!
render_pipeline,
}

Using a pipeline

If you run your program now, it’ll take a little longer to start, but it will still show the blue screen we got in the last section. That’s because while we created the render_pipeline, we need to modify the code in render() to actually use it.

如果你现在运行你的程序,它将需要更长的时间来启动,但它仍然会显示我们在上一节中得到的蓝屏。这是因为当我们创建render_pipeline时,我们需要修改render()中的代码以实际使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// render()

// ...
{
// 1.
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
// This is what [[location(0)]] in the fragment shader targets
wgpu::RenderPassColorAttachment {
view: &frame.view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(
wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}
),
store: true,
}
}
],
depth_stencil_attachment: None,
});

// NEW!
render_pass.set_pipeline(&self.render_pipeline); // 2.
render_pass.draw(0..3, 0..1); // 3.
}
// ...

We didn’t change much, but let’s talk about what we did change.
我们变化不大,但让我们谈谈我们改变了什么。

  1. We renamed _render_pass to render_pass and made it mutable.
  2. We set the pipeline on the render_pass using the one we just created.
  3. We tell wgpu to draw something with 3 vertices, and 1 instance. This is where [[builtin(vertex_index)]] comes from.
  1. 我们将_render_pass重命名为render_pass并使其可变。
  2. 我们将刚才创建的pipeline设置在render_pass上。
  3. 我们告诉wgpu用3个顶点和1个实例绘制一些东西。这就是[[builtin(vertex_index)]]的来源。

With all that you should be seeing a lovely brown triangle.
所有这些,你应该看到一个可爱的棕色三角形。

Challenge

Create a second pipeline that uses the triangle’s position data to create a color that it then sends to the fragment shader. Have the app swap between these when you press the spacebar. Hint: you’ll need to modify VertexOutput

创建第二条管道,该管道使用三角形的位置数据创建颜色,然后发送到片段着色器。按空格键时,让应用程序在这两者之间切换。提示:您需要修改VertexOutput

Check out the code!