cocos creator 登录流程

  1. 安装minigame-api-typings

    1
    npm install minigame-api-typings
  2. 代码中引用import ‘minigame-api-typings’;

  3. 调用wx登录接口

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    wx.login({
    complete: (res) => {
    console.log("login complete:" + JSON.stringify(res));
    },
    fail: (res) => {
    console.log("login fail:" + JSON.stringify(res));
    },
    success: (login_res) => {
    console.log("login success:" + JSON.stringify(login_res));
    wx.getSetting({
    complete: (res) => {
    console.log("authSetting complete:", JSON.stringify(res));
    },
    fail: (res) => {
    console.log("authSetting fail:", JSON.stringify(res));
    },
    success: (res) => {
    console.log("authSetting:", JSON.stringify(res));
    if (res.authSetting['scope.userInfo']) {
    this.get_user_info_login(login_res.code)
    }
    else {
    let wxSize = wx.getSystemInfoSync();
    let btn = wx.createUserInfoButton({
    type: 'text',
    text: '微信登录',
    style: {
    left: wxSize.screenWidth / 2 - 100,
    top: wxSize.screenHeight / 2 - 40,
    width: 200,
    height: 40,
    lineHeight: 40,
    backgroundColor: '#ffffff',
    borderColor: '#ffffff',
    borderWidth: 1,
    color: '#000000',
    textAlign: 'center',
    fontSize: 16,
    borderRadius: 4
    }
    });
    btn.onTap((res) => {
    console.log("createUserInfoButton:" + JSON.stringify(res));
    this.get_user_info_login(login_res.code)
    btn.destroy();
    });
    }
    }
    });
    }
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    get_user_info_login(code:string) {
    wx.getUserInfo({
    withCredentials:false,
    success: (result) => {
    login.login_player_no_author(code, result.userInfo.nickName, result.userInfo.avatarUrl);
    },
    fail: (res) => {
    console.log("fail:" + JSON.stringify(res));
    },
    complete: (res) => {
    console.log("complete:" + JSON.stringify(res));
    }
    });
    }

    login_player_no_author接口上传code到服务器端,服务器端收到code之后通过http请求到微信后台验证(AppID,Secret从微信小游戏管理后台获取):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={AppID}&secret={Secret}&js_code={code}&grant_type=authorization_code";
    var result = await HttpClientWrapper.GetRspAsync(url);

    if (result != null && result.StatusCode == System.Net.HttpStatusCode.OK)
    {
    var ret = await result.Content.ReadAsStringAsync();
    var ret_obj = Newtonsoft.Json.JsonConvert.DeserializeObject<Hashtable>(ret);
    var token = await player.client_Mng.token_player_login(ret_obj["openid"] as string);
    rsp.rsp(token);
    }

While we can tell that our scene is 3d because of our camera, it still feels very flat. That’s because our model stays the same color regardless of how it’s oriented. If we want to change that we need to add lighting to our scene.

虽然我们可以看出,我们的场景是3d的,因为我们的相机,它仍然感觉非常平坦。这是因为我们的模型保持相同的颜色,无论它是如何定向的。如果我们想改变这一点,我们需要向场景中添加照明。

In the real world, a light source emits photons which bounce around until they enter into our eyes. The color we see is the light’s original color minus whatever energy it lost while it was bouncing around.

在现实世界中,光源发射光子,这些光子在周围反弹,直到它们进入我们的眼睛。我们看到的颜色是光的原始颜色减去它在反弹时损失的能量。

In the computer graphics world, modeling individual photons would be hilariously computationally expensive. A single 100 Watt light bulb emits about 3.27 x 10^20 photons per second. Just imagine that for the sun! To get around this, we’re gonna use math to cheat.

在计算机图形学领域,对单个光子进行建模在计算上会非常昂贵。一个100瓦的灯泡每秒发射大约3.27 x 10^20个光子。想象一下,为了太阳!为了避开这个问题,我们要用数学来作弊。

Let’s discuss a few options.

让我们讨论几个选项。

Ray/Path Tracing

This is an advanced topic, and we won’t be covering it in depth here. It’s the closest model to the way light really works so I felt I had to mention it. Check out the ray tracing tutorial if you want to learn more.

The Blinn-Phong Model

Ray/path tracing is often too computationally expensive for most realtime applications (though that is starting to change), so a more efficient, if less accurate method based on the Phong reflection model is often used. It splits up the lighting calculation into three (3) parts: ambient lighting, diffuse lighting, and specular lighting. We’re going to be learning the Blinn-Phong model, which cheats a bit at the specular calculation to speed things up.

对于大多数实时应用程序来说,光线/路径跟踪在计算上往往过于昂贵(尽管这一点已经开始改变),因此通常会使用一种基于Phong reflection model的效率更高但精度更低的方法。它将照明计算分为三部分:环境照明、漫反射照明和镜面反射照明。我们将学习Blinn-Phong model,它在镜面反射计算中有点作弊,以加快速度。

Before we can get into that though, we need to add a light to our scene.

在我们开始之前,需要在场景中添加灯光。

1
2
3
4
5
6
7
8
9
// main.rs
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct LightUniform {
position: [f32; 3],
// Due to uniforms requiring 16 byte (4 float) spacing, we need to use a padding field here
_padding: u32,
color: [f32; 3],
}

Our LightUniform represents a colored point in space. We’re just going to use pure white light, but it’s good to allow different colors of light.

我们的LightUniform代表空间中的一个彩色点。我们将使用纯白光,但允许不同颜色的光是很好的。

We’re going to create another buffer to store our light in.

我们将创建另一个缓冲区来存储光线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let light_uniform = LightUniform {
position: [2.0, 2.0, 2.0],
_padding: 0,
color: [1.0, 1.0, 1.0],
};

// We'll want to update our lights position, so we use COPY_DST
let light_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Light VB"),
contents: bytemuck::cast_slice(&[light_uniform]),
usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
}
);

Don’t forget to add the light_uniform and light_buffer to State. After that we need to create a bind group layout and bind group for our light.

别忘了将light_uniform和light_buffer添加到State。之后,我们需要为灯光创建bind group layout和bind group。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let light_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: None,
});

let light_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &light_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: light_buffer.as_entire_binding(),
}],
label: None,
});

Add those to State, and also update the render_pipeline_layout.

将这些添加到State,并更新render_pipeline_layout。

1
2
3
4
5
6
7
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
bind_group_layouts: &[
&texture_bind_group_layout,
&camera_bind_group_layout,
&light_bind_group_layout,
],
});

Let’s also update the lights position in the update() method, so we can see what our objects look like from different angles.

我们还将更新update()方法中的灯光位置,以便从不同角度查看对象的外观。

1
2
3
4
5
6
// Update the light
let old_position: cgmath::Vector3<_> = self.light_uniform.position.into();
self.light_uniform.position =
cgmath::Quaternion::from_axis_angle((0.0, 1.0, 0.0).into(), cgmath::Deg(1.0))
* old_position;
self.queue.write_buffer(&self.light_buffer, 0, bytemuck::cast_slice(&[self.light_uniform]));

This will have the light rotate around the origin one degree every frame.

这将使灯光每帧围绕原点旋转一度。

Seeing the light

For debugging purposes, it would be nice if we could see where the light is to make sure that the scene looks correct. We could adapt our existing render pipeline to draw the light, but it will likely get in the way. Instead we are going to extract our render pipeline creation code into a new function called create_render_pipeline().

出于调试目的,如果我们能够看到灯光的位置,以确保场景看起来正确,那就太好了。我们可以调整现有的渲染管道来绘制灯光,但这可能会造成阻碍。相反,我们将把渲染管道创建代码提取到一个名为create_render_pipeline()的新函数中。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
fn create_render_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
color_format: wgpu::TextureFormat,
depth_format: Option<wgpu::TextureFormat>,
vertex_layouts: &[wgpu::VertexBufferLayout],
shader: wgpu::ShaderModuleDescriptor,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(&shader);

device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "main",
buffers: vertex_layouts,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "main",
targets: &[wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState {
alpha: wgpu::BlendComponent::REPLACE,
color: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrite::ALL,
}],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
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,
},
depth_stencil: depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
})
}

We also need to change State::new() to use this function.

我们还需要更改State::new()以使用此函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let render_pipeline = {
let shader = wgpu::ShaderModuleDescriptor {
label: Some("Normal Shader"),
flags: wgpu::ShaderFlags::all(),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
};
create_render_pipeline(
&device,
&render_pipeline_layout,
sc_desc.format,
Some(texture::Texture::DEPTH_FORMAT),
&[model::ModelVertex::desc(), InstanceRaw::desc()],
shader,
)
};

We’re going to need to modify model::DrawModel to use our light_bind_group.

我们需要修改model::DrawModel以使用我们的light_bind_group。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
pub trait DrawModel<'a> {
fn draw_mesh(
&mut self,
mesh: &'a Mesh,
material: &'a Material,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
fn draw_mesh_instanced(
&mut self,
mesh: &'a Mesh,
material: &'a Material,
instances: Range<u32>,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);

fn draw_model(
&mut self,
model: &'a Model,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
fn draw_model_instanced(
&mut self,
model: &'a Model,
instances: Range<u32>,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
}

impl<'a, 'b> DrawModel<'b> for wgpu::RenderPass<'a>
where
'b: 'a,
{
fn draw_mesh(
&mut self,
mesh: &'b Mesh,
material: &'b Material,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.draw_mesh_instanced(mesh, material, 0..1, camera, light);
}

fn draw_mesh_instanced(
&mut self,
mesh: &'b Mesh,
material: &'b Material,
instances: Range<u32>,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.set_vertex_buffer(0, &mesh.vertex_buffer, 0, 0);
self.set_index_buffer(&mesh.index_buffer, 0, 0);
self.set_bind_group(0, &material.bind_group, &[]);
self.set_bind_group(1, camera, &[]);
self.set_bind_group(2, light, &[]);
self.draw_indexed(0..mesh.num_elements, 0, instances);
}

fn draw_model(
&mut self,
model: &'b Model,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.draw_model_instanced(model, 0..1, camera, light);
}

fn draw_model_instanced(
&mut self,
model: &'b Model,
instances: Range<u32>,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
for mesh in &model.meshes {
let material = &model.materials[mesh.material];
self.draw_mesh_instanced(mesh, material, instances.clone(), camera, light);
}
}
}

With that done we can create another render pipeline for our light.

完成后,我们可以为灯光创建另一个渲染管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let light_render_pipeline = {
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Light Pipeline Layout"),
bind_group_layouts: &[&camera_bind_group_layout, &light_bind_group_layout],
push_constant_ranges: &[],
});
let shader = wgpu::ShaderModuleDescriptor {
label: Some("Light Shader"),
flags: wgpu::ShaderFlags::all(),
source: wgpu::ShaderSource::Wgsl(include_str!("light.wgsl").into()),
};
create_render_pipeline(
&device,
&layout,
sc_desc.format,
Some(texture::Texture::DEPTH_FORMAT),
&[model::ModelVertex::desc()],
shader,
)
};

I chose to create a seperate layout for the light_render_pipeline, as it doesn’t need all the resources that the regular render_pipeline needs (main just the textures).

我选择为light_render_pipeline创建一个单独的layout,因为它不需要常规render_pipeline需要的所有资源(纹理)。

With that in place we need to write the actual shaders.

接着我们需要编写实际的着色器。

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
34
35
36
37
38
39
40
41
42
43
// Vertex shader

[[block]]
struct Camera {
view_proj: mat4x4<f32>;
};
[[group(0), binding(0)]]
var<uniform> camera: Camera;

[[block]]
struct Light {
position: vec3<f32>;
color: vec3<f32>;
};
[[group(1), binding(0)]]
var<uniform> light: Light;

struct VertexInput {
[[location(0)]] position: vec3<f32>;
};

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

[[stage(vertex)]]
fn main(
model: VertexInput,
) -> VertexOutput {
let scale = 0.25;
var out: VertexOutput;
out.clip_position = camera.view_proj * vec4<f32>(model.position * scale + light.position, 1.0);
out.color = light.color;
return out;
}

// Fragment shader

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

Now we could manually implement the draw code for the light in render(), but to keep with the pattern we developed, let’s create a new trait called DrawLight.

现在我们可以在render()中手动实现灯光的绘制代码,但是为了保持我们开发的模式,让我们创建一个名为DrawLight的新trait。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
pub trait DrawLight<'a> {
fn draw_light_mesh(
&mut self,
mesh: &'a Mesh,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
fn draw_light_mesh_instanced(
&mut self,
mesh: &'a Mesh,
instances: Range<u32>,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);

fn draw_light_model(
&mut self,
model: &'a Model,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
fn draw_light_model_instanced(
&mut self,
model: &'a Model,
instances: Range<u32>,
camera: &'a wgpu::BindGroup,
light: &'a wgpu::BindGroup,
);
}

impl<'a, 'b> DrawLight<'b> for wgpu::RenderPass<'a>
where
'b: 'a,
{
fn draw_light_mesh(
&mut self,
mesh: &'b Mesh,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.draw_light_mesh_instanced(mesh, 0..1, camera, light);
}

fn draw_light_mesh_instanced(
&mut self,
mesh: &'b Mesh,
instances: Range<u32>,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
self.set_bind_group(0, camera, &[]);
self.set_bind_group(1, light, &[]);
self.draw_indexed(0..mesh.num_elements, 0, instances);
}

fn draw_light_model(
&mut self,
model: &'b Model,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
self.draw_light_model_instanced(model, 0..1, camera, light);
}
fn draw_light_model_instanced(
&mut self,
model: &'b Model,
instances: Range<u32>,
camera: &'b wgpu::BindGroup,
light: &'b wgpu::BindGroup,
) {
for mesh in &model.meshes {
self.draw_light_mesh_instanced(mesh, instances.clone(), camera, light);
}
}
}

Finally we want to add Light rendering to our render passes.

最后,我们希望将灯光渲染添加到渲染过程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
impl State {
// ...
fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
// ...
render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..));

use crate::model::DrawLight; // NEW!
render_pass.set_pipeline(&self.light_render_pipeline); // NEW!
render_pass.draw_light_model(
&self.obj_model,
&self.camera_bind_group,
&self.light_bind_group,
); // NEW!

render_pass.set_pipeline(&self.render_pipeline);
render_pass.draw_model_instanced(
&self.obj_model,
0..self.instances.len() as u32,
&self.camera_bind_group,
&self.light_bind_group,
);
}

With all that we’ll end up with something like this.

有了这些,我们最终会得到这样的结果。

light-in-scene

Ambient Lighting

Light has a tendency to bounce around before entering our eyes. That’s why you can see in areas that are in shadow. Actually modeling this interaction is computationally expensive, so we cheat. We define an ambient lighting value that stands in for the light bouncing of other parts of the scene to light our objects.

光线在进入我们的眼睛之前有一种反弹的趋势。这就是为什么你可以在阴影中看到。实际上,对这种交互进行建模在计算上很昂贵,所以我们作弊。我们定义一个环境光照明值,该值代表场景其他部分的光反弹,以照亮我们的对象。

The ambient part is based on the light color as well as the object color. We’ve already added our light_bind_group, so we just need to use it in our shader. In shader.wgsl, add the following below the texture uniforms.

环境光部分基于灯光颜色和对象颜色。我们已经添加了灯光组,所以我们只需要在着色器中使用它。在shader.wgsl中,在纹理下方添加以下内容。

1
2
3
4
5
6
7
[[block]]
struct Light {
position: vec3<f32>;
color: vec3<f32>;
};
[[group(2), binding(0)]]
var<uniform> light: Light;

Then we need to update our main shader code to calculate and use the ambient color value.

然后我们需要更新我们的主着色器代码来计算和使用环境光颜色值。

1
2
3
4
5
6
7
8
9
10
11
12
[[stage(fragment)]]
fn main(in: VertexOutput) -> [[location(0)]] vec4<f32> {
let object_color: vec4<f32> = textureSample(t_diffuse, s_diffuse, in.tex_coords);

// We don't need (or want) much ambient light, so 0.1 is fine
let ambient_strength = 0.1;
let ambient_color = light.color * ambient_strength;

let result = ambient_color * object_color.xyz;

return vec4<f32>(result, object_color.a);
}

With that we should get something like the this.

这样我们就可以得到类似这样的东西。

ambient_lighting

Diffuse Lighting

Remember the normal vectors that were included with our model? We’re finally going to use them. Normals represent the direction a surface is facing. By comparing the normal of a fragment with a vector pointing to a light source, we get a value of how light/dark that fragment should be. We compare the vector using the dot product to get the cosine of the angle between them.

还记得我们模型中包含的法向量吗?我们终于要使用它们了。法线表示曲面面对的方向。通过将碎片的法线与指向光源的向量进行比较,我们可以得到该碎片的亮/暗程度。我们使用点积比较矢量,得到它们之间的夹角的余弦。

normal_diagram

If the dot product of the normal and light vector is 1.0, that means that the current fragment is directly inline with the light source and will receive the lights full intensity. A value of 0.0 or lower means that the surface is perpendicular or facing away from the light, and therefore will be dark.

如果法线和光向量的点积为1.0,则表示当前片段直接与光源对齐,并将接收全强度的光。值为0.0或更低表示曲面垂直或背向灯光,因此将变暗。

We’re going to need to pull in the normal vector into our shader.wgsl.

我们需要将法线向量拉入shader.wgsl。

1
2
3
4
5
struct VertexInput {
[[location(0)]] position: vec3<f32>;
[[location(1)]] tex_coords: vec2<f32>;
[[location(2)]] normal: vec3<f32>; // NEW!
};

We’re also going to want to pass that value, as well as the vertex’s position to the fragment shader.

我们还要将该值以及顶点的位置传递给片段着色器。

1
2
3
4
5
6
struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] tex_coords: vec2<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] world_position: vec3<f32>;
};

For now let’s just pass the normal directly as is. This is wrong, but we’ll fix it later.

现在让我们直接按原样传递法线。这是错误的,但我们稍后会解决它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[stage(vertex)]]
fn main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
let model_matrix = mat4x4<f32>(
instance.model_matrix_0,
instance.model_matrix_1,
instance.model_matrix_2,
instance.model_matrix_3,
);
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.world_normal = model.normal;
var world_position: vec4<f32> = model_matrix * vec4<f32>(model.position, 1.0);
out.world_position = world_position.xyz;
out.clip_position = camera.view_proj * world_position;
return out;
}

With that we can do the actual calculation. Below the ambient_color calculation, but above result, add the following.

这样我们就可以进行实际计算了。在ambient_color计算下方,但在结果上方,添加以下内容。

1
2
3
4
let light_dir = normalize(light.position - in.world_position);

let diffuse_strength = max(dot(in.world_normal, light_dir), 0.0);
let diffuse_color = light.color * diffuse_strength;

Now we can include the diffuse_color in the result.

现在我们可以在结果中包含漫反射颜色。

1
let result = (ambient_color + diffuse_color) * object_color.xyz;

With that we get something like this.

这样我们就得到了这样的东西。

ambient_diffuse_wrong

The normal matrix

Remember when I said passing the vertex normal directly to the fragment shader was wrong? Let’s explore that by removing all the cubes from the scene except one that will be rotated 180 degrees on the y-axis.

还记得我说过将顶点法线直接传递给片段着色器是错误的吗?让我们通过移除场景中的所有立方体来探索这一点,其中一个立方体将在y轴上旋转180度。

1
2
3
4
const NUM_INSTANCES_PER_ROW: u32 = 1;

// In the loop we create the instances in
let rotation = cgmath::Quaternion::from_axis_angle((0.0, 1.0, 0.0).into(), cgmath::Deg(180.0));

We’ll also remove the ambient_color from our lighting result.

我们还将从照明结果中删除环境光颜色。

1
let result = (diffuse_color) * object_color.xyz;

That should give us something that looks like this.

这应该给我们一些如下图所示的东西。

diffuse_wrong

This is clearly wrong as the light is illuminating the wrong side of the cube. This is because we aren’t rotating our normals with our object, so no matter what direction the object faces, the normals will always face the same way.

这显然是错误的,因为光线照亮了立方体的错误一侧。这是因为我们没有随对象旋转法线,所以无论对象朝向哪个方向,法线都将始终朝向相同的方向。

image

We need to use the model matrix to transform the normals to be in the right direction. We only want the rotation data though. A normal represents a direction, and should be a unit vector throughout the calculation. We can get our normals into the right direction using what is called a normal matrix.

我们需要使用模型矩阵将法线变换为正确的方向。我们只需要旋转数据。法线表示方向,在整个计算过程中应为单位向量。我们可以使用所谓的法线矩阵使法线指向正确的方向。

We could compute the normal matrix in the vertex shader, but that would involve inverting the model_matrix, and WGSL doesn’t actually have an inverse function. We would have to code our own. On top of that computing the inverse of a matrix is actually really expensive, especially doing that compututation for every vertex.

我们可以在顶点着色器中计算法线矩阵,但这需要反转模型_矩阵,WGSL实际上没有反函数。我们必须自己编写代码。除此之外,计算矩阵的逆实际上非常昂贵,尤其是对每个顶点进行计算。

Instead we’re going to create add a normal matrix field to InstanceRaw. Instead of inverting the model matrix, we’ll just using the the instances rotation to create a Matrix3.

相反,我们将创建一个法线矩阵字段,并将其添加到InstanceRaw。我们将使用实例旋转来创建Matrix3,而不是反转模型矩阵。

We using Matrix3 instead of Matrix4 as we only really need the rotation component of the matrix.

我们使用Matrix3代替Matrix4,因为我们只需要矩阵的旋转分量。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(dead_code)]
struct InstanceRaw {
model: [[f32; 4]; 4],
normal: [[f32; 3]; 3],
}

impl model::Vertex for InstanceRaw {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<InstanceRaw>() as wgpu::BufferAddress,
// We need to switch from using a step mode of Vertex to Instance
// This means that our shaders will only change to use the next
// instance when the shader starts processing a new instance
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
// While our vertex shader only uses locations 0, and 1 now, in later tutorials we'll
// be using 2, 3, and 4, for Vertex. We'll start at slot 5 not conflict with them later
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
// A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot
// for each vec4. We don't have to do this in code though.
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 6,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
// NEW!
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress,
shader_location: 9,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 19]>() as wgpu::BufferAddress,
shader_location: 10,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 22]>() as wgpu::BufferAddress,
shader_location: 11,
format: wgpu::VertexFormat::Float32x3,
},
],
}
}
}

We need to modify Instance to create the normal matrix.

我们需要修改实例来创建法线矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Instance {
position: cgmath::Vector3<f32>,
rotation: cgmath::Quaternion<f32>,
}

impl Instance {
fn to_raw(&self) -> InstanceRaw {
let model =
cgmath::Matrix4::from_translation(self.position) * cgmath::Matrix4::from(self.rotation);
InstanceRaw {
model: model.into(),
// NEW!
normal: cgmath::Matrix3::from(self.rotation).into(),
}
}
}

Now we need to reconstruct the normal matrix in the vertex shader.

现在我们需要在顶点着色器中重建法线矩阵。

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
34
35
36
37
38
39
40
41
42
43
struct InstanceInput {
[[location(5)]] model_matrix_0: vec4<f32>;
[[location(6)]] model_matrix_1: vec4<f32>;
[[location(7)]] model_matrix_2: vec4<f32>;
[[location(8)]] model_matrix_3: vec4<f32>;
// NEW!
[[location(9)]] normal_matrix_0: vec3<f32>;
[[location(10)]] normal_matrix_1: vec3<f32>;
[[location(11)]] normal_matrix_2: vec3<f32>;
};

struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] tex_coords: vec2<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] world_position: vec3<f32>;
};

[[stage(vertex)]]
fn main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
let model_matrix = mat4x4<f32>(
instance.model_matrix_0,
instance.model_matrix_1,
instance.model_matrix_2,
instance.model_matrix_3,
);
// NEW!
let normal_matrix = mat3x3<f32>(
instance.normal_matrix_0,
instance.normal_matrix_1,
instance.normal_matrix_2,
);
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.world_normal = normal_matrix * model.normal;
var world_position: vec4<f32> = model_matrix * vec4<f32>(model.position, 1.0);
out.world_position = world_position.xyz;
out.clip_position = camera.view_proj * world_position;
return out;
}

I’m currently doing things in world space. Doing things in view-space also known as eye-space, is more standard as objects can have lighting issues when they are further away from the origin. If we wanted to use view-space, we would have include the rotation due to the view matrix as well. We’d also have to transform our light’s position using something like view_matrix * model_matrix * light_position to keep the calculation from getting messed up when the camera moves.

我现在在世界空间里做事。在视图空间(也称为eye-space)中进行操作更为标准,因为当对象离原点较远时,可能会出现照明问题。如果我们想使用视图空间,我们还应该包括由于视图矩阵而产生的旋转。我们还必须使用视图矩阵模型矩阵灯光位置之类的东西来变换灯光的位置,以防止相机移动时计算出错。

There are advantages to using view space. The main one is when you have massive worlds doing lighting and other calculations in model spacing can cause issues as floating point precision degrades when numbers get really large. View space keeps the camera at the origin meaning all calculations will be using smaller numbers. The actual lighting math ends up the same, but it does require a bit more setup.

使用视图空间有很多优点。最主要的一个问题是,当有大量的世界在进行照明和其他模型间距计算时,可能会导致问题,因为当数字变得非常大时,浮点精度会下降。视图空间将相机保持在原点,这意味着所有计算都将使用较小的数字。实际的照明数学结果是一样的,但它确实需要更多的设置。

With that change our lighting now looks correct.

通过这一更改,我们的照明现在看起来是正确的。

diffuse_right

Bringing back our other objects, and adding the ambient lighting gives us this.

带回其他对象,并添加环境照明,我们就可以做到这一点。

ambient_diffuse_lighting

Specular Lighting

Specular lighting describes the highlights that appear on objects when viewed from certain angles. If you’ve ever looked at a car, it’s the super bright parts. Basically, some of the light can reflect of the surface like a mirror. The location of the hightlight shifts depending on what angle you view it at.

镜面反射照明描述从特定角度查看时在对象上显示的高光。如果你看过一辆车,那就是超亮的部分。基本上,一些光可以像镜子一样反射表面。强光的位置会根据您的观察角度发生变化。

image

Because this is relative to the view angle, we are going to need to pass in the camera’s position both into the fragment shader and into the vertex shader.

因为这是相对于视图角度的,所以我们需要将摄影机的位置传递到片段着色器和顶点着色器中。

1
2
3
4
5
6
7
[[block]]
struct Camera {
view_pos: vec4<f32>;
view_proj: mat4x4<f32>;
};
[[group(1), binding(0)]]
var<uniform> camera: Camera;

Don’t forget to update the Camera struct in light.wgsl as well, as if it doesn’t match the CameraUniform struct in rust, the light will render wrong.

不要忘记更新light.wgsl中的Camera结构,因为如果它与rust中的CameraUniform结构不匹配,灯光将渲染错误。

We’re going to need to update the CameraUniform struct as well.

们还需要更新CameraUniform结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// main.rs
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct CameraUniform {
view_position: [f32; 4],
view_proj: [[f32; 4]; 4],
}

impl CameraUniform {
fn new() -> Self {
Self {
view_position: [0.0; 4],
view_proj: cgmath::Matrix4::identity().into(),
}
}

fn update_view_proj(&mut self, camera: &Camera) {
// We're using Vector4 because of the uniforms 16 byte spacing requirement
self.view_position = camera.eye.to_homogeneous();
self.view_proj = OPENGL_TO_WGPU_MATRIX * camera.build_view_projection_matrix();
}
}

Since we want to use our uniforms in the fragment shader now, we need to change it’s visibility.

因为我们现在想在片段着色器中使用制服,所以需要更改其可见性。

1
2
3
4
5
6
7
8
9
10
11
12
// main.rs
let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutBinding {
// ...
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, // Updated!
// ...
},
// ...
],
label: None,
});

We’re going to get the direction from the fragment’s position to the camera, and use that with the normal to calculate the reflect_dir.

我们将得到从碎片的位置到摄像机的方向,并将其与法线一起计算反射方向。

1
2
3
// In the fragment shader...
let view_dir = normalize(camera.view_pos.xyz - in.world_position);
let reflect_dir = reflect(-light_dir, in.world_normal);

Then we use the dot product to calculate the specular_strength and use that to compute the specular_color.

然后我们使用点积来计算镜面反射强度,并使用它来计算镜面反射颜色。

1
2
let specular_strength = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
let specular_color = specular_strength * light.color;

Finally we add that to the result.

最后,我们将其添加到结果中。

1
let result = (ambient_color + diffuse_color + specular_color) * object_color.xyz;

With that you should have something like this.

这样你就应该有这样的东西。

ambient_diffuse_specular_lighting

如果我们只看镜面反射的颜色会如下所示。

specular_lighting

The half direction

Up to this point we’ve actually only implemented the Phong part of Blinn-Phong. The Phong reflection model works well, but it can break down under certain circumstances. The Blinn part of Blinn-Phong comes from the realization that if you add the view_dir, and light_dir together, normalize the result and use the dot product of that and the normal, you get roughly the same results without the issues that using reflect_dir had.

到目前为止,我们实际上只实现了Blinn Phong的Phong部分。Phong反射模型运行良好,但在某些情况下可能会崩溃。Blinn Phong的Blinn部分源于这样一种认识,即如果将view_dir和light_dir添加在一起,对结果进行规格化,并使用其与法线的点积,则得到的结果与使用reflect_dir得到的结果大致相同,而不存在使用reflect_dir的问题。

1
2
3
4
let view_dir = normalize(camera.view_pos.xyz - in.world_position);
let half_dir = normalize(view_dir + light_dir);

let specular_strength = pow(max(dot(in.world_normal, half_dir), 0.0), 32.0);

It’s hard to tell the difference, but here’s the results.

很难区分两者之间的区别,但结果如下。

half_dir

Check out the code!

Let’s take a closer look at the last example.

让我们仔细看看最后一个例子。

forest_with_zoom

Models that should be in the back are getting rendered ahead of ones that should be in the front. This is caused by the draw order. By default, pixel data from a new object will replace old pixel data.

应该在后面的模型将在应该在前面的模型之前进行渲染。这是由提取顺序引起的。默认情况下,来自新对象的像素数据将替换旧像素数据。

There are two ways to solve this: sort the data from back to front, use what’s known as a depth buffer.

有两种方法可以解决这个问题:从后到前对数据进行排序,使用深度缓冲区。

Sorting from back to front

This is the go to method for 2d rendering as it’s pretty easier to know what’s supposed to go in front of what. You can just use the z order. In 3d rendering it gets a little more tricky because the order of the objects changes based on the camera angle.

这是2d渲染的方法,因为它很容易知道在什么之前应该做什么。你可以使用z顺序。在3d渲染中,由于对象的顺序会根据摄影机角度发生变化,因此会变得有点棘手。

A simple way of doing this is to sort all the objects by their distance to the cameras position. There are flaws with this method though as when a large object is behind a small object, parts of the large object that should be in front of the small object will be rendered behind. We’ll also run into issues with objects that overlap themselves.

执行此操作的一种简单方法是根据对象到摄影机位置的距离对所有对象进行排序。这种方法存在缺陷,尽管当一个大对象在一个小对象后面时,应该在小对象前面的大对象部分将在后面渲染。我们还将遇到对象重叠的问题。

If want to do this properly we need to have pixel level precision. That’s where a depth buffer comes in.

如果要正确地做到这一点,我们需要有像素级的精度。这就是深度缓冲区的作用。

A pixels depth

A depth buffer is a black and white texture that stores the z-coordinate of rendered pixels. Wgpu can use this when drawing new pixels to determine whether to replace the data or keep it. This technique is called depth testing. This will fix our draw order problem without needing us to sort our objects!

深度缓冲区是存储渲染像素z坐标的黑白纹理。Wgpu可以在绘制新像素时使用此选项来确定是替换数据还是保留数据。这种技术称为深度测试。这将解决我们的绘图顺序问题,而不需要我们对对象进行排序!

Let’s make a function to create the depth texture in texture.rs.

让我们制作一个函数,在texture.rs中创建深度纹理。

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
34
35
36
37
38
39
40
impl Texture {
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; // 1.

pub fn create_depth_texture(device: &wgpu::Device, sc_desc: &wgpu::SwapChainDescriptor, label: &str) -> Self {
let size = wgpu::Extent3d { // 2.
width: sc_desc.width,
height: sc_desc.height,
depth_or_array_layers: 1,
};
let desc = wgpu::TextureDescriptor {
label: Some(label),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: Self::DEPTH_FORMAT,
usage: wgpu::TextureUsage::RENDER_ATTACHMENT // 3.
| wgpu::TextureUsage::SAMPLED,
};
let texture = device.create_texture(&desc);

let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(
&wgpu::SamplerDescriptor { // 4.
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
compare: Some(wgpu::CompareFunction::LessEqual), // 5.
lod_min_clamp: -100.0,
lod_max_clamp: 100.0,
..Default::default()
}
);

Self { texture, view, sampler }
}
}
  1. We need the DEPTH_FORMAT for when we create the depth stage of the render_pipeline and creating the depth texture itself.
  2. Our depth texture needs to be the same size as our screen if we want things to render correctly. We can use our sc_desc to make sure that our depth texture is the same size as our swap chain images.
  3. Since we are rendering to this texture, we need to add the RENDER_ATTACHMENT flag to it.
  4. We technically don’t need a sampler for a depth texture, but our Texture struct requires it, and we need one if we ever want to sample it.
  5. If we do decide to render our depth texture, we need to use CompareFunction::LessEqual. This is due to how the samplerShadow and sampler2DShadow() interacts with the texture() function in GLSL.
  1. 当我们创建render_pipeline的depth stage和创建depth texture本身时,我们需要DEPTH_FORMAT。
  2. 如果我们想正确渲染,我们的深度纹理需要与屏幕大小相同。我们可以使用sc_desc确保深度纹理与swap chain图像的大小相同。
  3. 由于我们正在渲染此纹理,因此需要向其添加RENDER_ATTACHMENT标志。
  4. 从技术上讲,我们不需要深度纹理的采样器,但我们的纹理结构需要它,如果我们想对它进行采样,我们需要一个采样器。
  5. 如果我们决定渲染深度纹理,我们需要使用CompareFunction::LessEqual。这将决定samplerShadow和sampler2DShadow()如何与GLSL中的texture()函数交互。

We create our depth_texture in State::new().

我们在State::new()中创建深度纹理。

1
let depth_texture = texture::Texture::create_depth_texture(&device, &sc_desc, "depth_texture");

We need to modify our render_pipeline to allow depth testing.

我们需要修改渲染管线以允许深度测试。

1
2
3
4
5
6
7
8
9
10
11
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
// ...
depth_stencil: Some(wgpu::DepthStencilState {
format: texture::Texture::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less, // 1.
stencil: wgpu::StencilState::default(), // 2.
bias: wgpu::DepthBiasState::default(),
}),
// ...
});
  1. The depth_compare function tells us when to discard a new pixel. Using LESS means pixels will be drawn front to back. Here are all the values you can use.

    深度比较功能告诉我们何时丢弃新像素。使用LESS的方法将前后绘制像素。以下是您可以使用的所有值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[repr(C)]
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CompareFunction {
Undefined = 0,
Never = 1,
Less = 2,
Equal = 3,
LessEqual = 4,
Greater = 5,
NotEqual = 6,
GreaterEqual = 7,
Always = 8,
}
  1. There’s another type of buffer called a stencil buffer. It’s common practice to store the stencil buffer and depth buffer in the same texture. This fields control values for stencil testing. Since we aren’t using a stencil buffer, we’ll use default values. We’ll cover stencil buffers later.

    还有另一种类型的缓冲区称为stencil缓冲区。通常的做法是将stencil缓冲区和深度缓冲区存储在同一纹理中。此字段控制stencil测试的值。因为我们不使用stencil缓冲区,所以我们将使用默认值。稍后我们将讨论stencil缓冲区。

Don’t forget to store the depth_texture in State.

1
2
3
4
Self {
// ...
depth_texture,
}

We need to remember to change the resize() method to create a new depth_texture and depth_texture_view.

我们需要记住更改resize()方法以创建新的depth_texture和depth_texture_view。

Make sure you update the depth_texture after you update sc_desc. If you don’t, your program will crash as the depth_texture will be a different size than the swap_chain texture.

请确保在更新sc_desc后更新depth_texture。如果不更新,程序将崩溃,因为depth_texture的大小将不同于swap_chain texture。

The last change we need to make is in the render() function. We’ve created the depth_texture, but we’re not currently using it. We use it by attaching it to the depth_stencil_attachment of a render pass.

我们需要做的最后一个更改是在render()函数中。我们已经创建了depth_texture,但目前没有使用它。我们通过将其附加到渲染过程的depth_stencil_attachment来使用它。

1
2
3
4
5
6
7
8
9
10
11
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
// ...
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_texture.view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: true,
}),
stencil_ops: None,
}),
});

And that’s all we have to do! No shader code needed! If you run the application, the depth issues will be fixed.

这就是我们要做的!不需要着色器代码!如果运行应用程序,深度问题将得到修复。

forest_fixed

Challenge

Since the depth buffer is a texture, we can sample it in the shader. Because it’s a depth texture, we’ll have to use the samplerShadow uniform type and the sampler2DShadow function instead of sampler, and sampler2D respectively. Create a bind group for the depth texture (or reuse an existing one), and render it to the screen.

由于深度缓冲区是一个纹理,我们可以在着色器中对其进行采样。因为它是一个深度纹理,所以我们必须分别使用samplerShadow uniform类型和sampler2DShadow函数,而不是sampler和sampler2D。为深度纹理创建bind group(或重用现有组),并将其渲染到屏幕上。

Check out the code!

Our scene right now is very simple: we have one object centered at (0,0,0). What if we wanted more objects? This is were instancing comes in.

我们现在的场景非常简单:我们有一个以(0,0,0)为中心的对象。如果我们想要更多的东西呢?这是实例的目标。

Instancing allows us to draw the same object multiple times with different properties (position, orientation, size, color, etc.). There are multiple ways of doing instancing. One way would be to modify the uniform buffer to include these properties and then update it before we draw each instance of our object.

实例化允许我们使用不同的属性(位置、方向、大小、颜色等)多次绘制同一对象。有多种方法可以进行实例化。一种方法是修改uniform缓冲区以包含这些属性,然后在绘制对象的每个实例之前更新它。

We don’t want to use this method for performance reasons. Updating the uniform buffer for each instance would require multiple buffer copies each frame. On top of that, our method to update the uniform buffer currently requires use to create a new buffer to store the updated data. That’s a lot of time wasted between draw calls.

出于性能原因,我们不希望使用此方法。更新每个实例的uniform缓冲区将需要每个帧有多个缓冲区副本。除此之外,我们更新uniform缓冲区的方法目前需要创建一个新的缓冲区来存储更新后的数据。在两次draw calls之间浪费了很多时间。

If we look at the parameters for the draw_indexed function in the wgpu docs, we can see a solution to our problem.

如果我们查看wgpu文档中draw_indexed函数的参数,我们可以看到问题的解决方案。

1
2
3
4
5
6
pub fn draw_indexed(
&mut self,
indices: Range<u32>,
base_vertex: i32,
instances: Range<u32> // <-- This right here
)

The instances parameter takes a Range. This parameter tells the GPU how many copies, or instances, of our model we want to draw. Currently we are specifying 0..1, which instructs the GPU to draw our model once, and then stop. If we used 0..5, our code would draw 5 instances.

instances的参数为Range。此参数告诉GPU我们要绘制模型的多少副本或实例。目前我们正在指定0..1,它指示GPU绘制一次我们的模型,然后停止。如果我们使用0..5,我们的代码将绘制5个实例。

The fact that instances is a Range may seem weird as using 1..2 for instances would still draw 1 instance of our object. Seems like it would be simpler to just use a u32 right? The reason it’s a range is because sometimes we don’t want to draw all of our objects. Sometimes we want to draw a selection of them, because others are not in frame, or we are debugging and want to look at a particular set of instances.

instances是一个Range的事实可能看起来很奇怪,因为使用1..2 for instances仍然会绘制对象的一个实例。看起来使用u32会更简单,对吗?它是一个Range的原因是因为有时我们不想绘制所有的对象。有时,我们希望选择它们,因为其他的不在视野中,或者我们正在调试并希望查看一组特定的实例。

Ok, now we know how to draw multiple instances of an object, how do we tell wgpu what particular instance to draw? We are going to use something known as an instance buffer.

好的,现在我们知道了如何绘制一个对象的多个实例,我们如何告诉wgpu要绘制的特定实例?我们将使用实例缓冲区。

The Instance Buffer

We’ll create an instance buffer in a similar way to how we create a uniform buffer. First we’ll create a struct called Instance.

我们将以类似于创建uniform缓冲区的方式创建instance缓冲区。首先,我们将创建一个名为Instance的结构。

1
2
3
4
5
6
7
8
// main.rs
// ...

// NEW!
struct Instance {
position: cgmath::Vector3<f32>,
rotation: cgmath::Quaternion<f32>,
}

A Quaternion is a mathematical structure often used to represent rotation. The math behind them is beyond me (it involves imaginary numbers and 4D space) so I won’t be covering them here. If you really want to dive into them here’s a Wolfram Alpha article.

四元数是一种常用于表示旋转的数学结构。它们背后的数学是我无法理解的(它涉及虚数和4D空间),所以我不会在这里讨论它们。如果你真的想深入了解它们,这里有一篇Wolfram Alpha文章

Using these values directly in the shader would be a pain as quaternions don’t have a WGSL analog. I don’t feel like writing the math in the shader, so we’ll convert the Instance data into a matrix and store it into a struct called InstanceRaw.

直接在着色器中使用这些值会很痛苦,因为WGSL没有四元数模拟。我不想在着色器中编写数学,所以我们将实例数据转换为矩阵,并将其存储到名为InstanceRaw的结构中。

1
2
3
4
5
6
// NEW!
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct InstanceRaw {
model: [[f32; 4]; 4],
}

This is the data that will go into the wgpu::Buffer. We keep these separate so that we can update the Instance as much as we want without needing to mess with matrices. We only need to update the raw data before we draw.

这是将进入wgpu::Buffer的数据。我们将它们分开,这样我们就可以随心所欲地更新实例,而无需弄乱矩阵。我们只需要在绘制之前更新原始数据。

Let’s create a method on Instance to convert to InstanceRaw.

1
2
3
4
5
6
7
8
// NEW!
impl Instance {
fn to_raw(&self) -> InstanceRaw {
InstanceRaw {
model: (cgmath::Matrix4::from_translation(self.position) * cgmath::Matrix4::from(self.rotation)).into(),
}
}
}

Now we need to add 2 fields to State: instances, and instance_buffer.

1
2
3
4
struct State {
instances: Vec<Instance>,
instance_buffer: wgpu::Buffer,
}

We’ll create the instances in new(). We’ll use some constants to simplify things. We’ll display our instances in 10 rows of 10, and they’ll be spaced evenly apart.

我们将在new()中创建实例。我们将使用一些常量来简化事情。我们将以10行10列的形式显示我们的实例,并且它们将均匀地间隔开。

1
2
3
const NUM_INSTANCES_PER_ROW: u32 = 10;
const NUM_INSTANCES: u32 = NUM_INSTANCES_PER_ROW * NUM_INSTANCES_PER_ROW;
const INSTANCE_DISPLACEMENT: cgmath::Vector3<f32> = cgmath::Vector3::new(NUM_INSTANCES_PER_ROW as f32 * 0.5, 0.0, NUM_INSTANCES_PER_ROW as f32 * 0.5);

Now we can create the actual instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
impl State {
async fn new(window: &Window) -> Self {
// ...
let instances = (0..NUM_INSTANCES_PER_ROW).flat_map(|z| {
(0..NUM_INSTANCES_PER_ROW).map(move |x| {
let position = cgmath::Vector3 { x: x as f32, y: 0.0, z: z as f32 } - INSTANCE_DISPLACEMENT;

let rotation = if position.is_zero() {
// this is needed so an object at (0, 0, 0) won't get scaled to zero
// as Quaternions can effect scale if they're not created correctly
cgmath::Quaternion::from_axis_angle(cgmath::Vector3::unit_z(), cgmath::Deg(0.0))
} else {
cgmath::Quaternion::from_axis_angle(position.normalize(), cgmath::Deg(45.0))
};

Instance {
position, rotation,
}
})
}).collect::<Vec<_>>();
// ...
}
}

Now that we have our data, we can create the actual instance_buffer.

1
2
3
4
5
6
7
8
let instance_data = instances.iter().map(Instance::to_raw).collect::<Vec<_>>();
let instance_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Instance Buffer"),
contents: bytemuck::cast_slice(&instance_data),
usage: wgpu::BufferUsage::VERTEX,
}
);

We’re going to need to create a new VertexBufferLayout for InstanceRaw.

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
34
35
36
37
38
39
impl InstanceRaw {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<InstanceRaw>() as wgpu::BufferAddress,
// We need to switch from using a step mode of Vertex to Instance
// This means that our shaders will only change to use the next
// instance when the shader starts processing a new instance
step_mode: wgpu::InputStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
// While our vertex shader only uses locations 0, and 1 now, in later tutorials we'll
// be using 2, 3, and 4, for Vertex. We'll start at slot 5 not conflict with them later
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
// A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot
// for each vec4. We'll have to reassemble the mat4 in
// the shader.
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 6,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}

We need to add this descriptor to the render pipeline so that we can use it when we render.

1
2
3
4
5
6
7
8
9
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
// ...
vertex: wgpu::VertexState {
// ...
// UPDATED!
buffers: &[Vertex::desc(), InstanceRaw::desc()],
},
// ...
});

Don’t forget to return our new variables!

1
2
3
4
5
6
Self {
// ...
// NEW!
instances,
instance_buffer,
}

The last change we need to make is in the render() method. We need to bind our instance_buffer and we need to change the range we’re using in draw_indexed() to include the number of instances.

我们需要做的最后一个更改是render()方法。我们需要绑定实例缓冲区,并且需要更改在draw_indexed()中使用的范围,以包括实例数。

1
2
3
4
5
6
7
8
9
10
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
render_pass.set_bind_group(1, &self.camera_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
// NEW!
render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);

// UPDATED!
render_pass.draw_indexed(0..self.num_indices, 0, 0..self.instances.len() as _);

Make sure if you add new instances to the Vec, that you recreate the instance_buffer and as well as camera_bind_group, otherwise your new instances won’t show up correctly.

如果向Vec添加新实例,请确保重新创建instance_buffer和camera_bind_group,否则新实例将无法正确显示。

We need to reference the parts of our new matrix in shader.wgsl so that we can use it for our instances. Add the following to the top of shader.wgsl.

我们需要在shader.wgsl中引用新矩阵的部分,以便将其用于实例。将以下内容添加到shader.wgsl的顶部。

1
2
3
4
5
6
struct InstanceInput {
[[location(5)]] model_matrix_0: vec4<f32>;
[[location(6)]] model_matrix_1: vec4<f32>;
[[location(7)]] model_matrix_2: vec4<f32>;
[[location(8)]] model_matrix_3: vec4<f32>;
};

We need to reassemble the matrix before we can use it.

我们需要重新组装矩阵才能使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
[[stage(vertex)]]
fn main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
let model_matrix = mat4x4<f32>(
instance.model_matrix_0,
instance.model_matrix_1,
instance.model_matrix_2,
instance.model_matrix_3,
);
// Continued...
}

We’ll apply the model_matrix before we apply camera_uniform.view_proj. We do this because the camera_uniform.view_proj changes the coordinate system from world space to camera space. Our model_matrix is a world space transformation, so we don’t want to be in camera space when using it.

在应用camera_uniform.view_proj项目之前,我们将应用model_matrix。我们这样做是因为camera_uniform.view_proj将坐标系从世界空间更改为camera空间。我们的model_matrix是一个世界空间变换,所以我们不希望在使用它时处于摄影机空间。

1
2
3
4
5
6
7
8
9
10
11
[[stage(vertex)]]
fn main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
// ...
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0);
return out;
}

With all that done, we should have a forest of trees!

trees

Challenge

Modify the position and/or rotation of the instances every frame.

Check out the code!

While all of our previous work has seemed to be in 2d, we’ve actually been working in 3d the entire time! That’s part of the reason why our Vertex structure has position be an array of 3 floats instead of just 2. We can’t really see the 3d-ness of our scene, because we’re viewing things head on. We’re going to change our point of view by creating a Camera.

虽然我们之前的所有工作似乎都是二维的,但实际上我们一直都在三维中工作!这就是为什么我们的顶点结构的位置是一个由3个浮点数组成的数组而不是2个浮点数的部分原因。我们无法真正看到场景的3d效果,因为我们是正面观看的。我们将通过创建一个摄影机来改变我们的视角。

A perspective camera

This tutorial is more about learning to use wgpu and less about linear algebra, so I’m going to gloss over a lot of the math involved. There’s plenty of reading material online if you’re interested in what’s going on under the hood. The first thing to know is that we need cgmath = “0.18” in our Cargo.toml.

本教程更多的是关于学习使用wgpu,而不是关于线性代数,所以我将对涉及的许多数学进行简单讲解。如果你对幕后发生的事情感兴趣,网上有很多阅读材料。首先要知道的是,我们需要在Cargo.toml中使用cgmath=”0.18”。

Now that we have a math library, let’s put it to use! Create a Camera struct above the State struct.

现在我们有了一个数学库,让我们使用它吧!在struct State上方创建struct Camera。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Camera {
eye: cgmath::Point3<f32>,
target: cgmath::Point3<f32>,
up: cgmath::Vector3<f32>,
aspect: f32,
fovy: f32,
znear: f32,
zfar: f32,
}

impl Camera {
fn build_view_projection_matrix(&self) -> cgmath::Matrix4<f32> {
// 1.
let view = cgmath::Matrix4::look_at_rh(self.eye, self.target, self.up);
// 2.
let proj = cgmath::perspective(cgmath::Deg(self.fovy), self.aspect, self.znear, self.zfar);

// 3.
return OPENGL_TO_WGPU_MATRIX * proj * view;
}
}

The build_view_projection_matrix is where the magic happens.

build_view_projection_matrix就是魔法发生的地方。

  1. The view matrix moves the world to be at the position and rotation of the camera. It’s essentialy an inverse of whatever the transform matrix of the camera would be.
  2. The proj matrix wraps the scene to give the effect of depth. Without this, objects up close would be the same size as objects far away.
  3. The coordinate system in Wgpu is based on DirectX, and Metal’s coordinate systems. That means that in normalized device coordinates the x axis and y axis are in the range of -1.0 to +1.0, and the z axis is 0.0 to +1.0. The cgmath crate (as well as most game math crates) are built for OpenGL’s coordinate system. This matrix will scale and translate our scene from OpenGL’s coordinate sytem to WGPU’s. We’ll define it as follows.
  1. 视图矩阵将世界移动到摄影机的位置并旋转。它本质上是摄像机变换矩阵的逆矩阵。
  2. proj矩阵包裹场景以提供深度效果。如果不这样做,近处的对象将与远处的对象大小相同。
  3. Wgpu中的坐标系基于DirectX和Metal的坐标系。这意味着在标准化设备坐标中,x轴和y轴在-1.0到+1.0的范围内,z轴在0.0到+1.0的范围内。cgmath crate(以及大多数游戏数学库)是为OpenGL的坐标系构建的。该矩阵将缩放并将场景从OpenGL坐标系统转换为WGPU坐标系统。我们将对其进行如下定义。
1
2
3
4
5
6
7
#[rustfmt::skip]
pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4<f32> = cgmath::Matrix4::new(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.0, 0.0, 0.5, 1.0,
);
  • Note: We don’t explicitly need the OPENGL_TO_WGPU_MATRIX, but models centered on (0, 0, 0) will be halfway inside the clipping area. This is only an issue if you aren’t using a camera matrix.
  • 注: 我们并不明确需要OPENGL_TO_WGPU_MATRIX,但是以(0,0,0)为中心的模型将位于剪裁区域的一半。这只是一个问题,如果你不使用相机矩阵。

Now let’s add a camera field to State.

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
struct State {
// ...
camera: Camera,
// ...
}

async fn new(window: &Window) -> Self {
// let diffuse_bind_group ...

let camera = Camera {
// position the camera one unit up and 2 units back
// +z is out of the screen
eye: (0.0, 1.0, 2.0).into(),
// have it look at the origin
target: (0.0, 0.0, 0.0).into(),
// which way is "up"
up: cgmath::Vector3::unit_y(),
aspect: sc_desc.width as f32 / sc_desc.height as f32,
fovy: 45.0,
znear: 0.1,
zfar: 100.0,
};

Self {
// ...
camera,
// ...
}
}

Now that we have our camera, and it can make us a view projection matrix, we need somewhere to put it. We also need some way of getting it into our shaders.

现在我们有了相机,它可以使我们成为一个视图投影矩阵,我们需要把它放在某个地方。我们还需要某种方法将其放入着色器中。

The uniform buffer

Up to this point we’ve used Buffers to store our vertex and index data, and even to load our textures. We are going to use them again to create what’s known as a uniform buffer. A uniform is a blob of data that is available to every invocation of a set of shaders. We’ve technically already used uniforms for our texture and sampler. We’re going to use them again to store our view projection matrix. To start let’s create a struct to hold our uniform.

到目前为止,我们已经使用缓冲区来存储顶点和索引数据,甚至加载纹理。我们将再次使用它们来创建所谓的uniform缓冲区。uniform是一组着色器的每次调用都可用的数据块。从技术上讲,我们已经为我们的纹理和取样器使用了uniforms。我们将再次使用它们来存储视图投影矩阵。首先,让我们创建一个用于保存uniforms的struct。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// We need this for Rust to store our data correctly for the shaders
#[repr(C)]
// This is so we can store this in a buffer
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct CameraUniform {
// We can't use cgmath with bytemuck directly so we'll have
// to convert the Matrix4 into a 4x4 f32 array
view_proj: [[f32; 4]; 4],
}

impl CameraUniform {
fn new() -> Self {
use cgmath::SquareMatrix;
Self {
view_proj: cgmath::Matrix4::identity().into(),
}
}

fn update_view_proj(&mut self, camera: &Camera) {
self.view_proj = camera.build_view_projection_matrix().into();
}
}

Now that we have our data structured, let’s make our camera_buffer.

1
2
3
4
5
6
7
8
9
10
11
12
// in new() after creating `camera`

let mut camera_uniform = CameraUniform::new();
camera_uniform.update_view_proj(&camera);

let camera_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Camera Buffer"),
contents: bytemuck::cast_slice(&[camera_uniform]),
usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
}
);

Uniform buffers and bind groups

Cool, now that we have a uniform buffer, what do we do with it? The answer is we create a bind group for it. First we have to create the bind group layout.

既然我们有了一个uniform的缓冲区,我们该怎么处理它呢?答案是我们为它创建一个bind group。首先,我们必须创建bind group layout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
],
label: Some("camera_bind_group_layout"),
});
  1. We only really need camera information in the vertex shader, as that’s what we’ll use to manipulate our vertices.
  2. The dynamic field indicates whether this buffer will change size or not. This is useful if we want to store an array of things in our uniforms.
  1. 我们只在顶点着色器中需要摄影机信息,因为我们将使用这些信息来操纵顶点。
  2. dynamic字段指示此缓冲区是否将更改大小。如果我们想在制服中存储一系列的东西,这是很有用的。

Now we can create the actual bind group.

1
2
3
4
5
6
7
8
9
10
let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &camera_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
}
],
label: Some("camera_bind_group"),
});

Like with our texture, we need to register our camera_bind_group_layout with the render pipeline.

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

Now we need to add camera_buffer and camera_bind_group to State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct State {
// ...
camera: Camera,
camera_uniform: CameraUniform,
camera_buffer: wgpu::Buffer,
camera_bind_group: wgpu::BindGroup,
}

async fn new(window: &Window) -> Self {
// ...
Self {
// ...
camera,
camera_uniform,
camera_buffer,
camera_bind_group,
}
}

The final thing we need to do before we get into shaders is use the bind group in render().

1
2
3
4
5
6
7
8
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
// NEW!
render_pass.set_bind_group(1, &self.camera_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);

render_pass.draw_indexed(0..self.num_indices, 0, 0..1);

Using the uniform in the vertex shader

Modify the vertex shader to include the following.

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
// Vertex shader

[[block]] // 1.
struct CameraUniform {
view_proj: mat4x4<f32>;
};
[[group(1), binding(0)]] // 2.
var<uniform> camera: Camera;

struct VertexInput {
[[location(0)]] position: vec3<f32>;
[[location(1)]] tex_coords: vec2<f32>;
};

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

[[stage(vertex)]]
fn main(
model: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = camera.view_proj * vec4<f32>(model.position, 1.0); // 3.
return out;
}
  1. According to the WGSL Spec, The block decorator indicates this structure type represents the contents of a buffer resource occupying a single binding slot in the shader’s resource interface. Any structure used as a uniform must be annotated with [[block]]
  2. Because we’ve created a new bind group, we need to specify which one we’re using in the shader. The number is determined by our render_pipeline_layout. The texture_bind_group_layout is listed first, thus it’s group(0), and camera_bind_group is second, so it’s group(1).
  3. Multiplication order is important when it comes to matrices. The vector goes on the right, and the matrices gone on the left in order of importance.
  1. 根据WGSL规范,block decorator指示此结构类型表示占用着色器资源接口中单个绑定槽的缓冲区资源的内容。任何用作uniform的结构都必须用[[block]]注释
  2. 因为我们已经创建了一个新的bind group,所以需要指定在着色器中使用的bind group。编号由渲染管道布局决定。纹理绑定组布局列在第一位,因此它是group(0),摄影机绑定组列在第二位,因此它是group(1)。
  3. 当涉及到矩阵时,乘法顺序很重要。向量在右边,矩阵按顺序在左边。

A controller for our camera

If you run the code right now, you should get something that looks like this.

static-tree

The shape’s less stretched now, but it’s still pretty static. You can experiment with moving the camera position around, but most cameras in games move around. Since this tutorial is about using wgpu and not how to process user input, I’m just going to post the CameraController code below.

该形状现在拉伸程度有所降低,但仍然相当静态。你可以尝试移动摄像机的位置,但游戏中的大多数摄像机都会移动。因为本教程是关于使用wgpu而不是如何处理用户输入的,所以我将在下面直接发布CameraController代码。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
struct CameraController {
speed: f32,
is_up_pressed: bool,
is_down_pressed: bool,
is_forward_pressed: bool,
is_backward_pressed: bool,
is_left_pressed: bool,
is_right_pressed: bool,
}

impl CameraController {
fn new(speed: f32) -> Self {
Self {
speed,
is_up_pressed: false,
is_down_pressed: false,
is_forward_pressed: false,
is_backward_pressed: false,
is_left_pressed: false,
is_right_pressed: false,
}
}

fn process_events(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::KeyboardInput {
input: KeyboardInput {
state,
virtual_keycode: Some(keycode),
..
},
..
} => {
let is_pressed = *state == ElementState::Pressed;
match keycode {
VirtualKeyCode::Space => {
self.is_up_pressed = is_pressed;
true
}
VirtualKeyCode::LShift => {
self.is_down_pressed = is_pressed;
true
}
VirtualKeyCode::W | VirtualKeyCode::Up => {
self.is_forward_pressed = is_pressed;
true
}
VirtualKeyCode::A | VirtualKeyCode::Left => {
self.is_left_pressed = is_pressed;
true
}
VirtualKeyCode::S | VirtualKeyCode::Down => {
self.is_backward_pressed = is_pressed;
true
}
VirtualKeyCode::D | VirtualKeyCode::Right => {
self.is_right_pressed = is_pressed;
true
}
_ => false,
}
}
_ => false,
}
}

fn update_camera(&self, camera: &mut Camera) {
use cgmath::InnerSpace;
let forward = camera.target - camera.eye;
let forward_norm = forward.normalize();
let forward_mag = forward.magnitude();

// Prevents glitching when camera gets too close to the
// center of the scene.
if self.is_forward_pressed && forward_mag > self.speed {
camera.eye += forward_norm * self.speed;
}
if self.is_backward_pressed {
camera.eye -= forward_norm * self.speed;
}

let right = forward_norm.cross(camera.up);

// Redo radius calc in case the up/ down is pressed.
let forward = camera.target - camera.eye;
let forward_mag = forward.magnitude();

if self.is_right_pressed {
// Rescale the distance between the target and eye so
// that it doesn't change. The eye therefore still
// lies on the circle made by the target and eye.
camera.eye = camera.target - (forward + right * self.speed).normalize() * forward_mag;
}
if self.is_left_pressed {
camera.eye = camera.target - (forward - right * self.speed).normalize() * forward_mag;
}
}
}

This code is not perfect. The camera slowly moves back when you rotate it. It works for our purposes though. Feel free to improve it!

这个代码并不完美。旋转相机时,相机会慢慢向后移动。不过,它对我们的目的是有效的。请随意改进它!

We still need to plug this into our existing code to make it do anything. Add the controller to State and create it in new().

我们仍然需要将其插入到现有代码中,使其能够执行任何操作。将控制器添加到State并在new()中创建它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct State {
// ...
camera: Camera,
// NEW!
camera_controller: CameraController,
// ...
}
// ...
impl State {
async fn new(window: &Window) -> Self {
// ...
let camera_controller = CameraController::new(0.2);
// ...

Self {
// ...
camera_controller,
// ...
}
}
}

We’re finally going to add some code to input() (assuming you haven’t already)!

最后,我们将向input()添加一些代码(假设您还没有这样做)!

1
2
3
fn input(&mut self, event: &WindowEvent) -> bool {
self.camera_controller.process_events(event)
}

Up to this point, the camera controller isn’t actually doing anything. The values in our uniform buffer need to be updated. There are a few main methods to do that.

到目前为止,相机控制器实际上什么都没有做。我们的uniform缓冲区中的值需要更新。有几种主要的方法可以做到这一点。

  1. We can create a separate buffer and copy it’s contents to our camera_buffer. The new buffer is known as a staging buffer. This method is usually how it’s done as it allows the contents of the main buffer (in this case camera_buffer) to only be accessible by the gpu. The gpu can do some speed optimizations which it couldn’t if we could access the buffer via the cpu.
  2. We can call on of the mapping method’s map_read_async, and map_write_async on the buffer itself. These allow us to access a buffer’s contents directly, but requires us to deal with the async aspect of these methods this also requires our buffer to use the BufferUsage::MAP_READ and/or BufferUsage::MAP_WRITE. We won’t talk about it here, but you check out Wgpu without a window tutorial if you want to know more.
  3. We can use write_buffer on queue.
  1. 我们可以创建一个单独的缓冲区,并将其内容复制到我们的相机缓冲区。新缓冲区称为暂存缓冲区。这种方法通常是这样做的,因为它只允许gpu访问主缓冲区(在本例中为camera_缓冲区)的内容。gpu可以做一些速度优化,如果我们可以通过cpu访问缓冲区,它就无法做到。
  2. 我们可以在缓冲区本身上调用映射方法的map_read_async和map_write_async。这些允许我们直接访问缓冲区的内容,但要求我们处理这些方法的异步问题。这还要求我们的缓冲区使用BufferUsage::MAP_READ读取和BufferUsage::MAP_WRITE。我们这里不讨论它,但是如果您想了解更多,可以在没有窗口教程的情况下查看Wgpu。
  3. 我们可以在队列上使用write_buffer。

We’re going to use option number 3.

1
2
3
4
5
fn update(&mut self) {
self.camera_controller.update_camera(&mut self.camera);
self.camera_uniform.update_view_proj(&self.camera);
self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[self.camera_uniform]));
}

That’s all we need to do. If you run the code now you should see a pentagon with our tree texture that you can rotate around and zoom into with the wasd/arrow keys.

这就是我们需要做的。如果你现在运行代码,你会看到一个五边形和我们的树纹理,你可以通过wasd/箭头键旋转和放大。

Challenge

让我们的模型独立于相机旋转。提示:您需要另一个矩阵。

Check out the code!

Up to this point we have been drawing super simple shapes. While we can make a game with just triangles, trying to draw highly detailed objects would massively limit what devices could even run our game. However, we can get around this problem with textures.

到目前为止,我们一直在画超级简单的形状。虽然我们可以制作一个只有三角形的游戏,但尝试绘制高度详细的对象将极大地限制哪些设备甚至可以运行我们的游戏。然而,我们可以通过纹理来解决这个问题。

Textures are images overlayed on a triangle mesh to make it seem more detailed. There are multiple types of textures such as normal maps, bump maps, specular maps and diffuse maps. We’re going to talk about diffuse maps, or more simply, the color texture.

纹理是叠加在三角形网格上的图像,使其看起来更详细。有多种类型的纹理,例如法线贴图、凹凸贴图、镜面反射贴图和漫反射贴图。我们将讨论漫反射贴图,或者更简单地说,颜色纹理。

Loading an image from a file

If we want to map an image to our mesh, we first need an image. Let’s use this happy little tree:

如果要将图像映射到网格,首先需要一个图像。让我们用这棵快乐的小树:

小树

We’ll use the image crate to load our tree. We already added to our dependencies in the first section, so all we have to do is use it.

我们将使用image crate来装载我们的树。我们已经在第一节中添加了依赖项,所以我们所要做的就是使用它。

In State’s new() method add the following just after creating the swap_chain:

在State的new()方法中,在创建swap_chain之后添加以下内容:

1
2
3
4
5
6
7
8
9
let swap_chain = device.create_swap_chain(&surface, &sc_desc);
// NEW!

let diffuse_bytes = include_bytes!("happy-tree.png");
let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap();
let diffuse_rgba = diffuse_image.as_rgba8().unwrap();

use image::GenericImageView;
let dimensions = diffuse_image.dimensions();

Here we grab the bytes from our image file and load them into an image which is then converted into a Vec of rgba bytes. We also save the image’s dimensions for when we create the actual Texture.

在这里,我们从图像文件中获取bytes,并将它们加载到图像中,然后将图像转换为rgba字节的Vec。我们还保存图像的尺寸,以便在创建实际纹理时使用。

Now, let’s create the Texture:

现在,让我们创建纹理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let texture_size = wgpu::Extent3d {
width: dimensions.0,
height: dimensions.1,
depth_or_array_layers: 1,
};
let diffuse_texture = device.create_texture(
&wgpu::TextureDescriptor {
// All textures are stored as 3D, we represent our 2D texture
// by setting depth to 1.
size: texture_size,
mip_level_count: 1, // We'll talk about this a little later
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
// Most images are stored using sRGB so we need to reflect that here.
format: wgpu::TextureFormat::Rgba8UnormSrgb,
// SAMPLED tells wgpu that we want to use this texture in shaders
// COPY_DST means that we want to copy data to this texture
usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST,
label: Some("diffuse_texture"),
}
);

Getting data into a Texture

The Texture struct has no methods to interact with the data directly. However, we can use a method on the queue we created earlier called write_texture to load the texture in. Let’s take a look at how we do that:

纹理结构没有直接与数据交互的方法。但是,我们可以在前面创建的队列上使用一个名为write_texture的方法来加载纹理。让我们来看看我们是如何做到这一点的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
queue.write_texture(
// Tells wgpu where to copy the pixel data
wgpu::ImageCopyTexture {
texture: &diffuse_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
// The actual pixel data
diffuse_rgba,
// The layout of the texture
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: std::num::NonZeroU32::new(4 * dimensions.0),
rows_per_image: std::num::NonZeroU32::new(dimensions.1),
},
texture_size,
);

The old way of writing data to a texture was to copy the pixel data to a buffer and then copy it to the texture. Using write_texture is a bit more efficient as it uses one less buffer - I’ll leave it here though in case you need it.

将数据写入纹理的旧方法是将像素数据复制到缓冲区,然后将其复制到纹理。使用write_texture更有效,因为它使用的缓冲区少了一个,不过我会把它放在这里,以备您需要。

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
let buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Temp Buffer"),
contents: &diffuse_rgba,
usage: wgpu::BufferUsage::COPY_SRC,
}
);

let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("texture_buffer_copy_encoder"),
});

encoder.copy_buffer_to_texture(
wgpu::ImageCopyBuffer {
buffer: &buffer,
offset: 0,
bytes_per_row: 4 * dimensions.0,
rows_per_image: dimensions.1,
},
wgpu::ImageCopyTexture {
texture: &diffuse_texture,
mip_level: 0,
array_layer: 0,
origin: wgpu::Origin3d::ZERO,
},
size,
);

queue.submit(std::iter::once(encoder.finish()));

The bytes_per_row field needs some consideration. This value needs to be a multiple of 256. Check out the gif tutorial for more details.

每行字节数字段需要考虑。此值必须是256的倍数。有关详细信息,请查看gif教程。

TextureViews and Samplers

Now that our texture has data in it, we need a way to use it. This is where a TextureView and a Sampler come in. A TextureView offers us a view into our texture. A Sampler controls how the Texture is sampled. Sampling works similar to the eyedropper tool in GIMP/Photoshop. Our program supplies a coordinate on the texture (known as a texture coordinate), and the sampler then returns the corresponding color based on the texture and some internal parameters.

既然我们的纹理中有数据,我们需要一种使用它的方法。这就是TextureView和采样器的作用。纹理视图为我们提供了纹理视图。采样器控制纹理的采样方式。采样的工作原理类似于GIMP/Photoshop中的滴管工具。我们的程序在纹理上提供一个坐标(称为纹理坐标),然后采样器根据纹理和一些内部参数返回相应的颜色。

Let’s define our diffuse_texture_view and diffuse_sampler now:

现在让我们定义diffuse_texture_view和diffuse_sampler:

1
2
3
4
5
6
7
8
9
10
11
12
// We don't need to configure the texture view much, so let's
// let wgpu define it.
let diffuse_texture_view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});

The address_mode_* parameters determine what to do if the sampler gets a texture coordinate that’s outside of the texture itself. We have a few options to choose from:

address_mode_*参数决定了如果采样器获得的纹理坐标位于纹理本身之外,该怎么办。我们有几个选项可供选择:

  • ClampToEdge: Any texture coordinates outside the texture will return the color of the nearest pixel on the edges of the texture.
  • Repeat: The texture will repeat as texture coordinates exceed the textures dimensions.
  • MirrorRepeat: Similar to Repeat, but the image will flip when going over boundaries.
  • ClampToEdge:纹理外部的任何纹理坐标都将返回纹理边缘上最近像素的颜色。
  • Repeat:纹理坐标超过纹理尺寸时,纹理将重复。
  • MirrorRepeat: 与“重复”类似,但图像在越过边界时将翻转。

sampler

The mag_filter and min_filter options describe what to do when a fragment covers multiple pixels, or there are multiple fragments for a single pixel. This often comes into play when viewing a surface from up close, or from far away.

mag_filter和min_filter选项描述了当一个片段覆盖多个像素,或者一个像素有多个片段时该怎么做。这通常在从近距离或远处查看曲面时起作用。

There are 2 options:

有两种选择:

  • Linear: Attempt to blend the in-between fragments so that they seem to flow together.
  • Nearest: In-between fragments will use the color of the nearest pixel. This creates an image that’s crisper from far away, but pixelated up close. This can be desirable, however, if your textures are designed to be pixelated, like in pixel art games, or voxel games like Minecraft.
  • Linear: 尝试将片段混合在一起,使它们看起来是一起流动的。
  • Nearest: 在片段之间,将使用最近像素的颜色。这将创建一个从远处看更清晰的图像,但在近距离内像素化。然而,如果你的纹理被设计成像素化,比如像素艺术游戏,或者像Minecraft这样的体素游戏,这可能是可取的。

Mipmaps are a complex topic, and will require their own section in the future. For now, we can say that mipmap_filter functions similar to (mag/min)_filter as it tells the sampler how to blend between mipmaps.

mipmap是一个复杂的主题,将来需要的部分讨论。现在,我们可以说mipmap_filter的功能类似于(mag/min)_filter,因为它告诉采样器如何在mipmap之间混合。

I’m using some defaults for the other fields. If you want to see what they are, check the wgpu docs.

我对其他字段使用了一些默认值。如果您想查看它们是什么,请查看wgpu文档

All these different resources are nice and all, but they don’t do us much good if we can’t plug them in anywhere. This is where BindGroups and PipelineLayouts come in.

所有这些不同的资源都很好,但是如果我们不能在任何地方插入它们,它们对我们没有多大好处。这就是BindGroups和PipelineLayouts的用武之地。

The BindGroup

A BindGroup describes a set of resources and how they can be accessed by a shader. We create a BindGroup using a BindGroupLayout. Let’s make one of those first.

BindGroup描述一组资源以及着色器如何访问这些资源。我们使用BindGroupLayout创建一个BindGroup。让我们先做一个。

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
let texture_bind_group_layout = device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::Sampler {
// This is only for TextureSampleType::Depth
comparison: false,
// This should be true if the sample_type of the texture is:
// TextureSampleType::Float { filterable: true }
// Otherwise you'll get an error.
filtering: true,
},
count: None,
},
],
label: Some("texture_bind_group_layout"),
}
);

Our texture_bind_group_layout has two entries: one for a sampled texture at binding 0, and one for a sampler at binding 1. Both of these bindings are visible only to the fragment shader as specified by FRAGMENT. The possible values for this field are any bitwise combination of NONE, VERTEX, FRAGMENT, or COMPUTE. Most of the time we’ll only use FRAGMENT for textures and samplers, but it’s good to know what else is available.

我们的texture_bind_group_layout有两个条目:一个用于绑定0处的sampled texture,另一个用于绑定1处的sampler。这两个绑定仅对片段指定的片段着色器可见。visibility字段的可能值是NONE、VERTEX、FRAGMENT或COMPUTE的任意位组合。大多数情况下,我们只会对片段使用纹理和采样器,但最好知道还有什么可用的。

With texture_bind_group_layout, we can now create our BindGroup:

使用texture_bind_group_layout,我们现在可以创建BindGroup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let diffuse_bind_group = device.create_bind_group(
&wgpu::BindGroupDescriptor {
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
}
],
label: Some("diffuse_bind_group"),
}
);

Looking at this you might get a bit of déjà vu! That’s because a BindGroup is a more specific declaration of the BindGroupLayout. The reason why they’re separate is it allows us to swap out BindGroups on the fly, so long as they all share the same BindGroupLayout. Each texture and sampler we create will need to be added to a BindGroup. For our purposes, we’ll create a new bind group for each texture.

看看这个,你可能会有点似曾相识!这是因为BindGroup是BindGroupLayout的更具体的声明。它们分开的原因是它允许我们动态地交换BindGroups,只要它们都共享相同的BindGroupLayout。我们创建的每个纹理和采样器都需要添加到BindGroup中。出于我们的目的,我们将为每个纹理创建一个新的绑定组。

Now that we have our diffuse_bind_group, let’s add it to our State struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct State {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
size: winit::dpi::PhysicalSize<u32>,
render_pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
num_indicies: u32,
diffuse_bind_group: wgpu::BindGroup, // NEW!
}

And make sure we return these fields in the new method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
impl State {
async fn new() -> Self {
// ...
Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
render_pipeline,
vertex_buffer,
index_buffer,
num_indices,
// NEW!
diffuse_bind_group,
}
}
}

Now that we’ve got our BindGroup, we can use it in our render() function.

1
2
3
4
5
6
7
8
// render()
// ...
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); // NEW!
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);

render_pass.draw_indexed(0..self.num_indices, 0, 0..1);

PipelineLayout

Remember the PipelineLayout we created back in the pipeline section? Now we finally get to use it! The PipelineLayout contains a list of BindGroupLayouts that the pipeline can use. Modify render_pipeline_layout to use our texture_bind_group_layout.

还记得我们在管道部分创建的PipelineLayout吗?现在我们终于可以使用它了!PipelineLayout包含pipeline可以使用的BindGroupLayouts列表。修改render_pipeline_layout以使用texture_bind_group_layout。

1
2
3
4
5
6
7
8
9
10
11
async fn new(...) {
// ...
let render_pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"), // NEW!
bind_group_layouts: &[&texture_bind_group_layout], // NEW!
push_constant_ranges: &[],
}
);
// ...
}

A change to the VERTICES

There’s a few things we need to change about our Vertex definition. Up to now we’ve been using a color attribute to set the color of our mesh. Now that we’re using a texture, we want to replace our color with tex_coords. These coordinates will then be passed to the Sampler to retrieve the appropriate color.

关于顶点定义,我们需要更改一些内容。到目前为止,我们一直在使用颜色属性来设置网格的颜色。现在我们正在使用纹理,我们想用tex_coords替换我们的颜色。然后,这些坐标将传递给采样器以检索适当的颜色。

Since our tex_coords are two dimensional, we’ll change the field to take two floats instead of three.

由于tex_coords是二维的,因此我们将字段更改为采用两个浮点数,而不是三个浮点数。

First, we’ll change the Vertex struct:

首先,我们将更改顶点结构:

1
2
3
4
5
6
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
tex_coords: [f32; 2], // NEW!
}

And then reflect these changes in the VertexBufferLayout:

然后在VertexBufferLayout中反映这些更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
impl Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2, // NEW!
},
]
}
}
}

Lastly we need to change VERTICES itself. Replace the existing definition with the following:

最后,我们需要改变顶点本身。将现有定义替换为以下内容:

1
2
3
4
5
6
7
8
// Changed
const VERTICES: &[Vertex] = &[
Vertex { position: [-0.0868241, 0.49240386, 0.0], tex_coords: [0.4131759, 0.99240386], }, // A
Vertex { position: [-0.49513406, 0.06958647, 0.0], tex_coords: [0.0048659444, 0.56958646], }, // B
Vertex { position: [-0.21918549, -0.44939706, 0.0], tex_coords: [0.28081453, 0.050602943], }, // C
Vertex { position: [0.35966998, -0.3473291, 0.0], tex_coords: [0.85967, 0.15267089], }, // D
Vertex { position: [0.44147372, 0.2347359, 0.0], tex_coords: [0.9414737, 0.7347359], }, // E
];

Shader time

With our new Vertex structure in place it’s time to update our shaders. We’ll first need to pass our tex_coords into the vertex shader and then use them over to our fragment shader to get the final color from the Sampler. Let’s start with the vertex shader:

随着新顶点结构的就位,是时候更新着色器了。我们首先需要将tex_coords传递到顶点着色器,然后将它们传递到片段着色器,以从采样器获得最终颜色。让我们从顶点着色器开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Vertex shader

struct VertexInput {
[[location(0)]] position: vec3<f32>;
[[location(1)]] tex_coords: vec2<f32>;
};

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

[[stage(vertex)]]
fn main(
model: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = vec4<f32>(model.position, 1.0);
return out;
}

Now that we have our vertex shader outputting our tex_coords, we need to change the fragment shader to take them in. With these coordinates, we’ll finally be able to use our sampler to get a color from our texture.

现在我们已经有了顶点着色器输出tex_coords,我们需要更改片段着色器以接收它们。有了这些坐标,我们最终可以使用采样器从纹理中获取颜色。

1
2
3
4
5
6
7
8
9
10
11
// Fragment shader

[[group(0), binding(0)]]
var t_diffuse: texture_2d<f32>;
[[group(0), binding(1)]]
var s_diffuse: sampler;

[[stage(fragment)]]
fn main(in: VertexOutput) -> [[location(0)]] vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}

The variables t_diffuse and s_diffuse are what’s known as uniforms. We’ll go over uniforms more in the cameras section. For now, all we need to know is that group() corresponds to the 1st parameter in set_bind_group() and binding() relates to the binding specified when we created the BindGroupLayout and BindGroup.

变量t_diffuse和s_diffuse称为uniforms。我们将在照相机部分详细介绍uniforms。现在,我们只需要知道group()对应于set_bind_group()中的第一个参数,binding()与创建BindGroupLayout和BindGroup时指定的绑定相关。

The results

If we run our program now we should get the following result:

result

That’s weird, our tree is upside down! This is because wgpu’s world coordinates have the y-axis pointing up, while texture coordinates have the y-axis pointing down. In other words, (0, 0) in texture coordinates coresponds to the top-left of the image, while (1, 1) is the bottom right.

真奇怪,我们的树倒了!这是因为wgpu的世界坐标的y轴指向上,而纹理坐标的y轴指向下。换句话说,纹理坐标中的(0,0)对应于图像的左上角,而(1,1)对应于右下角。

纹理

We can get our triangle right-side up by inverting the y coordinate of each texture coordinate:

通过反转每个纹理坐标的y坐标,我们可以使三角形正面朝上:

1
2
3
4
5
6
7
8
const VERTICES: &[Vertex] = &[
// Changed
Vertex { position: [-0.0868241, 0.49240386, 0.0], tex_coords: [0.4131759, 0.00759614], }, // A
Vertex { position: [-0.49513406, 0.06958647, 0.0], tex_coords: [0.0048659444, 0.43041354], }, // B
Vertex { position: [-0.21918549, -0.44939706, 0.0], tex_coords: [0.28081453, 0.949397], }, // C
Vertex { position: [0.35966998, -0.3473291, 0.0], tex_coords: [0.85967, 0.84732914], }, // D
Vertex { position: [0.44147372, 0.2347359, 0.0], tex_coords: [0.9414737, 0.2652641], }, // E
];

With that in place, we now have our tree right-side up on our hexagon:

有了它,我们的树就在六边形的正确位置:

纹理

Cleaning things up

For convenience sake, let’s pull our texture code into its module. We’ll first need to add the anyhow crate to our Cargo.toml file to simplify error handling;

为了方便起见,让我们将纹理代码拉入其模块。我们首先需要将anyhow crate添加到Cargo.toml文件中,以简化错误处理;

1
2
3
4
5
6
7
8
9
10
[dependencies]
image = "0.23"
cgmath = "0.18"
winit = "0.25"
env_logger = "0.9"
log = "0.4"
pollster = "0.2"
wgpu = "0.9"
bytemuck = { version = "1.4", features = [ "derive" ] }
anyhow = "1.0" // NEW!

Then, in a new file called src/texture.rs, add the following:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
use image::GenericImageView;
use anyhow::*;

pub struct Texture {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
}

impl Texture {
pub fn from_bytes(
device: &wgpu::Device,
queue: &wgpu::Queue,
bytes: &[u8],
label: &str
) -> Result<Self> {
let img = image::load_from_memory(bytes)?;
Self::from_image(device, queue, &img, Some(label))
}

pub fn from_image(
device: &wgpu::Device,
queue: &wgpu::Queue,
img: &image::DynamicImage,
label: Option<&str>
) -> Result<Self> {
let rgba = img.as_rgba8().unwrap();
let dimensions = img.dimensions();

let size = wgpu::Extent3d {
width: dimensions.0,
height: dimensions.1,
depth_or_array_layers: 1,
};
let texture = device.create_texture(
&wgpu::TextureDescriptor {
label,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST,
}
);

queue.write_texture(
wgpu::ImageCopyTexture {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
rgba,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: std::num::NonZeroU32::new(4 * dimensions.0),
rows_per_image: std::num::NonZeroU32::new(dimensions.1),
},
size,
);

let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(
&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
}
);

Ok(Self { texture, view, sampler })
}
}

Note that we’re returning a CommandBuffer with our texture. This means we can load multiple textures at the same time, and then submit all their command buffers at once.

请注意,我们返回CommandBuffer使用纹理。这意味着我们可以同时加载多个纹理,然后一次提交它们的所有命令缓冲区。

We need to import texture.rs as a module, so somewhere at the top of main.rs add the following.

我们需要将texture.rs作为一个模块导入,因此在main.rs顶部的某处添加以下内容。

1
mod texture;

The texture creation code in new() now gets a lot simpler:

1
2
3
4
5
let swap_chain = device.create_swap_chain(&surface, &sc_desc);
let diffuse_bytes = include_bytes!("happy-tree.png"); // CHANGED!
let diffuse_texture = texture::Texture::from_bytes(&device, &queue, diffuse_bytes, "happy-tree.png").unwrap(); // CHANGED!

// Everything up until `let texture_bind_group_layout = ...` can now be removed.

We still need to store the bind group separately so that Texture doesn’t need know how the BindGroup is laid out. Creating the diffuse_bind_group changes slightly to use the view and sampler fields of our diffuse_texture:

我们仍然需要单独存储bind group,这样纹理就不需要知道BindGroup是如何布置的。创建diffuse_bind_group时会稍微更改,以使用diffuse_texture的view和sampler字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let diffuse_bind_group = device.create_bind_group(
&wgpu::BindGroupDescriptor {
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), // CHANGED!
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), // CHANGED!
}
],
label: Some("diffuse_bind_group"),
}
);

Finally, let’s update our State field to use our shiny new Texture struct, as we’ll need it in future tutorials.

最后,让我们更新State字段以使用闪亮的新纹理结构,因为我们将在未来的教程中需要它。

1
2
3
4
5
struct State {
// ...
diffuse_bind_group: wgpu::BindGroup,
diffuse_texture: texture::Texture, // NEW
}
1
2
3
4
5
6
7
8
9
10
11
impl State {
async fn new() -> Self {
// ...
Self {
// ...
num_indices,
diffuse_bind_group,
diffuse_texture, // NEW
}
}
}

Phew!

With these changes in place, the code should be working the same as it was before, but we now have a much easier way to create textures.

有了这些更改,代码应该和以前一样工作,但是我们现在有了一种更简单的方法来创建纹理。

Challenge

创建另一个纹理,并在按空格键时将其调出。

Check out the code!

We’re finally talking about them!

You were probably getting sick of me saying stuff like “we’ll get to that when we talk about buffers”. Well now’s the time to finally talk about buffers, but first…

你可能已经厌倦了我说的那些话,比如”当我们谈论缓冲区的时候,我们会说到那个”。现在是时候讨论缓冲区了,但是首先。。。

What is a buffer?

A buffer is a blob of data on the GPU. A buffer is guaranteed to be contiguous, meaning that all the data is stored sequentially in memory. Buffers are generally used to store simple things like structs or arrays, but it can store more complex stuff such as graph structures like trees (provided all the nodes are stored together and don’t reference anything outside of the buffer). We are going to use buffers a lot, so let’s get started with two of the most important ones: the vertex buffer, and the index buffer.

缓冲区是GPU上的一团数据。缓冲区保证是连续的,这意味着所有数据都按顺序存储在内存中。缓冲区通常用于存储简单的东西,如结构或数组,但它可以存储更复杂的东西,如树等图形结构(前提是所有节点都存储在一起,并且不引用缓冲区之外的任何内容)。我们将大量使用缓冲区,因此让我们从两个最重要的缓冲区开始:顶点缓冲区和索引缓冲区。

The vertex buffer

Previously we’ve stored vertex data directly in the vertex shader. While that worked fine to get our bootstraps on, it simply won’t do for the long-term. The types of objects we need to draw will vary in size, and recompiling the shader whenever we need to update the model would massively slow down our program. Instead we are going to use buffers to store the vertex data we want to draw. Before we do that though we need to describe what a vertex looks like. We’ll do this by creating a new struct.

之前,我们将顶点数据直接存储在顶点着色器中。虽然这样做可以很好地启动我们的引导系统,但从长远来看,这样做是行不通的。我们需要绘制的对象类型在大小上会有所不同,每当我们需要更新模型时重新编译着色器将大大降低我们的程序速度。相反,我们将使用缓冲区来存储要绘制的顶点数据。在我们这样做之前,我们需要描述一个顶点是什么样子的。我们将通过创建一个新的结构来实现这一点。

1
2
3
4
5
6
7
// main.rs
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct Vertex {
position: [f32; 3],
color: [f32; 3],
}

Our vertices will all have a position and a color. The position represents the x, y, and z of the vertex in 3d space. The color is the red, green, and blue values for the vertex. We need the Vertex to be copyable so we can create a buffer with it.

我们的顶点都有一个位置和颜色。该位置表示三维空间中顶点的x、y和z。颜色是顶点的红色、绿色和蓝色值。我们需要顶点是可复制的,这样我们就可以用它创建一个缓冲区。

Next we need the actual data that will make up our triangle. Below Vertex add the following.

接下来,我们需要组成三角形的实际数据。在顶点下方添加以下内容。

1
2
3
4
5
6
//main.rs
const VERTICES: &[Vertex] = &[
Vertex { position: [0.0, 0.5, 0.0], color: [1.0, 0.0, 0.0] },
Vertex { position: [-0.5, -0.5, 0.0], color: [0.0, 1.0, 0.0] },
Vertex { position: [0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] },
];

We arrange the vertices in counter clockwise order: top, bottom left, bottom right. We do it this way partially out of tradition, but mostly because we specified in the rasterization_state of the render_pipeline that we want the front_face of our triangle to be wgpu::FrontFace::Ccw so that we cull the back face. This means that any triangle that should be facing us should have its vertices in counter clockwise order.

我们按逆时针顺序排列顶点:顶部、左下角、右下角。我们这样做部分是出于传统,但主要是因为我们在渲染管道的光栅化状态中指定,我们希望三角形的前表面为wgpu::FrontFace::Ccw,以便剔除后表面。这意味着任何面向我们的三角形的顶点都应该按逆时针顺序排列。

Now that we have our vertex data, we need to store it in a buffer. Let’s add a vertex_buffer field to State.

现在我们有了顶点数据,我们需要将其存储在缓冲区中。让我们向State添加一个顶点vertex_buffer字段。

1
2
3
4
5
6
7
8
9
10
// main.rs
struct State {
// ...
render_pipeline: wgpu::RenderPipeline,

// NEW!
vertex_buffer: wgpu::Buffer,

// ...
}

Now let’s create the buffer in new().

现在让我们在new()中创建缓冲区。

1
2
3
4
5
6
7
8
// new()
let vertex_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsage::VERTEX,
}
);

To access the create_buffer_init method on wgpu::Device we’ll have to import the DeviceExt extension trait. For more information on extension traits, check out this article.

要访问wgpu::Device上的create_buffer_init方法,我们必须导入DeviceExt扩展特性。有关扩展特性的更多信息,请参阅本文。

To import the extension trait, this line somewhere near the top of main.rs.

要导入扩展特性,请在main.rs顶部附近的某个位置输入如下代码。

1
use wgpu::util::DeviceExt;

You’ll note that we’re using bytemuck to cast our VERTICES as a &[u8]. The create_buffer_init() method expects a &[u8], and bytemuck::cast_slice does that for us. Add the following to your Cargo.toml.

您会注意到,我们使用bytemuck将顶点转换为&[u8]。create_buffer_init()方法需要一个&[u8],而bytemuck::cast_slice则为我们这样做。将以下内容添加到Cargo.toml中。

1
bytemuck = { version = "1.4", features = [ "derive" ] }

We’re also going to need to implement two traits to get bytemuck to work. These are bytemuck::Pod and bytemuck::Zeroable. Pod indicates that our Vertex is “Plain Old Data”, and thus can be interpretted as a &[u8]. Zeroable indicates that we can use std::mem::zeroed(). We can modify our Vertex struct to derive these methods.

为了让bytemuck发挥作用,我们还需要实现两个特性。它们是bytemuck::Pod和bytemuck::Zeroable。Pod表示我们的顶点是“普通的旧数据”,因此可以解释为a&[u8]。Zeroable表示我们可以使用std::mem::zeroed()。我们可以修改顶点结构来派生这些方法。

1
2
3
4
5
6
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
color: [f32; 3],
}

If your struct includes types that don’t implement Pod and Zeroable, you’ll need to implement these traits manually. These traits don’t require us to implement any methods, so we just need to use the following to get our code to work.

如果您的结构包含未实现Pod和Zeroable的类型,则需要手动实现这些特性。这些特性不需要我们实现任何方法,所以我们只需要使用以下方法来让代码正常工作。

1
2
unsafe impl bytemuck::Pod for Vertex {}
unsafe impl bytemuck::Zeroable for Vertex {}

Finally we can add our vertex_buffer to our State struct.

最后,我们可以将顶点缓冲区添加到结构体State中。

1
2
3
4
5
6
7
8
9
10
Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
render_pipeline,
vertex_buffer,
}

So what do I do with it?

We need to tell the render_pipeline to use this buffer when we are drawing, but first we need to tell the render_pipeline how to read the buffer. We do this using VertexBufferLayouts and the vertex_buffers field that I promised we’d talk about when we created the render_pipeline.

我们需要告诉render_pipeline在绘图时使用此缓冲区,但首先我们需要告诉render_pipeline如何读取缓冲区。我们使用VertexBufferLayouts和vertex_buffers字段来实现这一点,我在创建render_pipeline时承诺过要讨论这个字段。

A VertexBufferLayout defines how a buffer is layed out in memory. Without this, the render_pipeline has no idea how to map the buffer in the shader. Here’s what the descriptor for a buffer full of Vertex would look like.

VertexBufferLayout定义缓冲区在内存中的布局方式。如果没有此选项,渲染管道将不知道如何映射着色器中的缓冲区。下面是一个充满顶点的缓冲区的描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, // 1.
step_mode: wgpu::InputStepMode::Vertex, // 2.
attributes: &[ // 3.
wgpu::VertexAttribute {
offset: 0, // 4.
shader_location: 0, // 5.
format: wgpu::VertexFormat::Float32x3, // 6.
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}
]
}
  1. The array_stride defines how wide a vertex is. When the shader goes to read the next vertex, it will skip over array_stride number of bytes. In our case, array_stride will probably be 24 bytes.
  2. step_mode tells the pipeline how often it should move to the next vertex. This seems redundant in our case, but we can specify wgpu::InputStepMode::Instance if we only want to change vertices when we start drawing a new instance. We’ll cover instancing in a later tutorial.
  3. Vertex attributes describe the individual parts of the vertex. Generally this is a 1:1 mapping with a struct’s fields, which it is in our case.
  4. This defines the offset in bytes that this attribute starts. The first attribute is usually zero, and any future attributes are the collective size_of the previous attributes data.
  5. This tells the shader what location to store this attribute at. For example layout(location=0) in vec3 x in the vertex shader would correspond to the position field of the struct, while layout(location=1) in vec3 x would be the color field.
  6. format tells the shader the shape of the attribute. Float3 corresponds to vec3 in shader code. The max value we can store in an attribute is Float4 (Uint4, and Int4 work as well). We’ll keep this in mind for when we have to store things that are bigger than Float4.
  1. array_stride定义顶点的宽度。当着色器读取下一个顶点时,它将跳过数组的字节数。在我们的例子中,数组的步长可能是24字节。
  2. step_mode告诉管道它应该移动到下一个顶点的频率。在我们的例子中,这似乎是多余的,但如果我们只想在开始绘制新实例时更改顶点,则可以指定wgpu::InputStepMode::Instance。我们将在后面的教程中介绍实例化。
  3. 顶点属性描述顶点的各个部分。通常,这是一个1:1映射,带有结构的字段,在我们的例子中就是这样。
  4. 这定义了该属性开始的偏移量(以字节为单位)。第一个属性通常为零,任何未来属性都是前一个属性数据的集合大小。
  5. 这将告知着色器存储此属性的位置。例如,顶点着色器中vec3 x中的布局(位置=0)将对应于结构的位置字段,而vec3 x中的布局(位置=1)将对应于颜色字段。
  6. format告诉着色器属性的布局。Float3对应于着色器代码中的vec3。我们可以存储在属性中的最大值是Float4(Uint4和Int4也可以使用)。当我们必须存储比Float4大的东西时,我们会注意这一点。

For you visually learners, our vertex buffer looks like this.

对于您来说,我们的顶点缓冲区如下所示。

顶点缓冲区

Let’s create a static method on Vertex that returns this descriptor.

让我们在顶点上创建一个静态方法,返回这个描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.rs
impl Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}
]
}
}
}

Specifying the attributes as we did now is quite verbose. We could use the vertex_attr_array macro provided by wgpu to clean things up a bit. With it our VertexBufferLayout becomes

像我们现在这样指定属性是相当冗长的。我们可以使用wgpu提供的vertex_attr_array宏稍微清理一下。有了它,我们的VertexBuffer布局变得

1
2
3
4
5
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3],
}

While this is definitely nice, we would have to change the lifetime on wgpu::VertexBufferLayout to ‘static as rust wouldn’t compile the code because the result of vertex_attr_array is a temporary value, which we can’t return from a function.

虽然这确实很好,但我们必须将wgpu::VertexBufferLayout上的生存期更改为“静态”,因为rust不会编译代码,因为vertex_attr_array数组的结果是一个临时值,我们无法从函数返回。

Beyond that, I feel it’s good to show how the data gets mapped, so I’ll forgo using this macro for now.

除此之外,我觉得展示数据是如何映射的很好,所以我现在放弃使用这个宏。

Now we can use it when we create the render_pipeline.

现在,我们可以在创建render_pipeline时使用它。

1
2
3
4
5
6
7
8
9
10
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
// ...
vertex: wgpu::VertexState {
// ...
buffers: &[
Vertex::desc(),
],
},
// ...
});

One more thing: we need to actually set the vertex buffer in the render method otherwise our program will crash.

还有一件事:我们需要在渲染方法中实际设置顶点缓冲区,否则我们的程序将崩溃。

1
2
3
4
5
// render()
render_pass.set_pipeline(&self.render_pipeline);
// NEW!
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.draw(0..3, 0..1);

set_vertex_buffer takes two parameters. The first is what buffer slot to use for this vertex buffer. You can have multiple vertex buffers set at a time.

set_vertex_buffer采用两个参数。第一个是用于此顶点缓冲区的缓冲槽。一次可以设置多个顶点缓冲区。

The second parameter is the slice of the buffer to use. You can store as many objects in a buffer as your hardware allows, so slice allows us to specify which portion of the buffer to use. We use .. to specify the entire buffer.

第二个参数是要使用的缓冲区的切片。在硬件允许的情况下,可以在缓冲区中存储任意多的对象,因此slice允许我们指定要使用的缓冲区部分。我们用..指定整个缓冲区。

Before we continue, we should change the render_pass.draw() call to use the number of vertices specified by VERTICES. Add a num_vertices to State, and set it to be equal to VERTICES.len().

在继续之前,我们应该更改render_pass.draw()调用以使用顶点指定的顶点数。将num_vertices添加到状态,并将其设置为等于VERTICES.len()。

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
// main.rs

struct State {
// ...
num_vertices: u32,
}

impl State {
// ...
fn new(...) -> Self {
// ...
let num_vertices = VERTICES.len() as u32;

Self {
surface,
device,
queue,
sc_desc,
swap_chain,
render_pipeline,
vertex_buffer,
num_vertices,
size,
}
}
}

Then use it in the draw call.

1
2
// render
render_pass.draw(0..self.num_vertices, 0..1);

Before our changes will have any effect, we need to update our vertex shader to get its data from the vertex buffer. We’ll also have it include the vertex color as well.

在更改产生任何效果之前,我们需要更新顶点着色器以从顶点缓冲区获取其数据。我们还将让它包括顶点颜色。

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
// Vertex shader

struct VertexInput {
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec3<f32>;
};

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

[[stage(vertex)]]
fn main(
model: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.color = model.color;
out.clip_position = vec4<f32>(model.position, 1.0);
return out;
}

// Fragment shader

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

If you’ve done things correctly, you should see a triangle that looks something like this.

如果你做的正确,你应该看到一个三角形,看起来像这样。

三角形

The index buffer

We technically don’t need an index buffer, but they still are plenty useful. An index buffer comes into play when we start using models with a lot of triangles. Consider this pentagon.

从技术上讲,我们不需要索引缓冲区,但它们仍然非常有用。当我们开始使用带有大量三角形的模型时,索引缓冲区就开始发挥作用。考虑一下这个五角大厦。

五角形

It has a total of 5 vertices, and 3 triangles. Now if we wanted to display something like this using just vertices we would need something like the following.

它总共有5个顶点和3个三角形。现在,如果我们只想用顶点来显示这样的东西,我们需要下面这样的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
const VERTICES: &[Vertex] = &[
Vertex { position: [-0.0868241, 0.49240386, 0.0], color: [0.5, 0.0, 0.5] }, // A
Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, // B
Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, // E

Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, // B
Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, // C
Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, // E

Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, // C
Vertex { position: [0.35966998, -0.3473291, 0.0], color: [0.5, 0.0, 0.5] }, // D
Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, // E
];

You’ll note though that some of the vertices are used more than once. C, and B get used twice, and E is repeated 3 times. Assuming that each float is 4 bytes, then that means of the 216 bytes we use for VERTICES, 96 of them are duplicate data. Wouldn’t it be nice if we could list these vertices once? Well we can! That’s were an index buffer comes into play.

但您会注意到,有些顶点被多次使用。C、 B使用两次,E重复三次。假设每个浮点是4个字节,那么这意味着我们用于顶点的216个字节中,有96个是重复数据。如果我们能列出这些顶点一次,不是很好吗?我们可以!这就是索引缓冲区发挥作用的原因。

Basically we store all the unique vertices in VERTICES and we create another buffer that stores indices to elements in VERTICES to create the triangles. Here’s an example of that with our pentagon.

基本上,我们将所有唯一的顶点存储在VERTICES中,并创建另一个缓冲区来存储顶点中元素的索引以创建三角形。这是我们五角大楼的一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.rs
const VERTICES: &[Vertex] = &[
Vertex { position: [-0.0868241, 0.49240386, 0.0], color: [0.5, 0.0, 0.5] }, // A
Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, // B
Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, // C
Vertex { position: [0.35966998, -0.3473291, 0.0], color: [0.5, 0.0, 0.5] }, // D
Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, // E
];

const INDICES: &[u16] = &[
0, 1, 4,
1, 2, 4,
2, 3, 4,
/* padding */ 0,
];

Now with this setup our VERTICES take up about 120 bytes and INDICES is just 18 bytes given that u16 is 2 bytes wide. We add 2 bytes padding as wgpu requires buffers to be aligned to 4 bytes. All together our pentagon is 134 bytes in total. That means we saved 82 bytes! It may not seem like much, but when dealing with tri counts in the hundreds of thousands, indexing saves a lot of memory.

现在,在这个设置中,我们的顶点占用了大约120个字节,而索引仅为18个字节,因为u16的宽度为2个字节。我们添加了2个字节的填充,因为wgpu要求缓冲区与4个字节对齐。我们的五角大楼总共有134字节。这意味着我们节省了82字节!它可能看起来不太多,但在处理数十万的三重计数时,索引可以节省大量内存。

There’s a couple of things we need to change in order to use indexing. The first is we need to create a buffer to store the indices. In State’s new() method create the index_buffer after you create the vertex_buffer. Also change num_vertices to num_indices and set it equal to INDICES.len().

为了使用索引,我们需要改变一些事情。首先,我们需要创建一个缓冲区来存储索引。在State的new()方法中,在创建顶点缓冲区之后创建索引缓冲区。还可以将num_vertices更改为num_indices,并将其设置为INDICES.len()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let vertex_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsage::VERTEX,
}
);
// NEW!
let index_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsage::INDEX,
}
);
let num_indices = INDICES.len() as u32;

We don’t need to implement Pod and Zeroable for our indices, because bytemuck has already implemented them for basic types such as u16. That means we can just add index_buffer and num_indices to the State struct.

我们不需要为索引实现Pod和Zeroable,因为bytemuck已经为u16等基本类型实现了它们。这意味着我们可以将index_buffer和num_indices添加到State结构中。

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

And then populate these fields in the constructor:

然后在构造函数中填充这些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
render_pipeline,
vertex_buffer,
// NEW!
index_buffer,
num_indices,
}

All we have to do now is update the render() method to use the index_buffer.

我们现在要做的就是更新render()方法以使用index_buffer。

1
2
3
4
5
// render()
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); // 1.
render_pass.draw_indexed(0..self.num_indices, 0, 0..1); // 2.

A couple things to note:

  1. The method name is set_index_buffer not set_index_buffers. You can only have one index buffer set at a time.
  2. When using an index buffer, you need to use draw_indexed. The draw method ignores the index buffer. Also make sure you use the number of indices (num_indices), not vertices as your model will either draw wrong, or the method will panic because there are not enough indices.

有几件事需要注意:

  1. 方法名称是set_index_buffer而不是set_index_buffers。一次只能设置一个索引缓冲区。
  2. 使用索引缓冲区时,需要使用draw_indexed。draw方法忽略索引缓冲区。还要确保使用的是索引的数量(num_index),而不是顶点,否则您的模型可能会绘制错误,或者该方法会因为索引不足而死机。

With all that you should have a garishly magenta pentagon in your window.

所有这些,你应该有一个华丽的洋红色五角大楼在你的窗口。

索引五角形

Color Correction

If you use a color picker on the magenta pentagon, you’ll get a hex value of #BC00BC. If you convert this to RGB values you’ll get (188, 0, 188). Dividing these values by 255 to get them into the [0, 1] range we get roughly (0.737254902, 0, 0.737254902). This is not the same as we are using for our vertex colors which is (0.5, 0.0, 0.5). The reason for this has to do with color spaces.

如果在洋红五角大楼上使用颜色选择器,将得到十六进制值#BC00BC。如果您将其转换为RGB值,您将得到(188,0188)。将这些值除以255得到[0,1]范围,我们大致得到(0.737254902,0,0.737254902)。这与我们使用的顶点颜色(0.5、0.0、0.5)不同。这与颜色空间有关。

Most monitors use a color space know as sRGB. Our swap chain is (most likely depending on what is returned from adapter.get_swap_chain_preferred_format()) using an sRGB texture format. The sRGB format stores colors according to their relative brightness instead of their actual brightness. The reason for this is that our eyes don’t perceive light linearly. We notice more differences in darker colors than we do lighter colors.

大多数显示器使用的颜色空间称为sRGB。我们的swap chain是(很可能取决于适配器返回的内容。adapter.get_swap_chain_preferred_format())使用sRGB纹理格式。sRGB格式根据颜色的相对亮度而不是实际亮度存储颜色。原因是我们的眼睛不能线性感知光线。我们注意到深色的差异比浅色的多。

You get an approximation of the correct color using the following formula: srgb_color = (rgb_color / 255) ^ 2.2. Doing this with an RGB value of (188, 0, 188) will give us (0.511397819, 0.0, 0.511397819). A little off from our (0.5, 0.0, 0.5). While you could tweak the formula to get the desired values, you’ll likely save a lot of time by using textures instead as they are stored as sRGB by default, so they don’t suffer from the same color inaccuracies that vertex colors do. We’ll cover textures in the next lesson.

使用以下公式可以获得正确颜色的近似值:srgb_color=(rgb_color/255)^2.2。在RGB值为(188,0188)的情况下执行此操作将得到(0.511397819,0.0,0.511397819)。离我们的(0.5,0.0,0.5)有点远。虽然可以调整公式以获得所需的值,但使用纹理可能会节省大量时间,因为默认情况下纹理存储为sRGB,因此它们不会像顶点颜色那样出现颜色不准确的情况。我们将在下一课中介绍纹理。

Challenge

Create a more complex shape than the one we made (aka. more than three triangles) using a vertex buffer and an index buffer. Toggle between the two with the space key.

创建一个比我们制作的形状更复杂的形状(aka。三个以上的三角形)使用顶点缓冲区和索引缓冲区。使用空格键在两者之间切换。

Check out the code!

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!

First, some house keeping: State

For convenience we’re going to pack all the fields into a struct, and create some methods on that.

为了方便起见,我们将把所有字段打包到一个结构中,并在此基础上创建一些方法。

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
34
// main.rs
use winit::window::Window;

struct State {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
size: winit::dpi::PhysicalSize<u32>,
}

impl State {
// Creating some of the wgpu types requires async code
async fn new(window: &Window) -> Self {
todo!()
}

fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
todo!()
}

fn input(&mut self, event: &WindowEvent) -> bool {
todo!()
}

fn update(&mut self) {
todo!()
}

fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
todo!()
}
}

I’m glossing over States fields, but they’ll make more sense as I explain the code behind the methods.

我对States字段进行了润色,但当我解释方法背后的代码时,它们会更有意义。

State::new()

The code for this is pretty straight forward, but let’s break this down a bit.

这方面的代码非常简单,但是让我们把它分解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl State {
// ...
async fn new(window: &Window) -> Self {
let size = window.inner_size();

// The instance is a handle to our GPU
// BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
let surface = unsafe { instance.create_surface(window) };
let adapter = instance.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
},
).await.unwrap();

The surface is used to create the swap_chain. Our window needs to implement raw-window-handle’s HasRawWindowHandle trait to access the native window implementation for wgpu to properly create the graphics backend. Fortunately, winit’s Window fits the bill. We also need it to request our adapter.

surface用于创建swap_chain。我们的窗口需要实现原始raw-window-handle’s HasRawWindowHandle特性,以便wgpu访问本机窗口实现,从而正确创建图形后端。幸运的是,winit的窗口符合要求。我们还需要它来请求适配器。

The options I’ve passed to request_adapter aren’t guaranteed to work for all devices, but will work for most of them. If you want to get all adapters for a particular backend you can use enumerate_adapters. This will give you an iterator that you can loop over to check if one of the adapters works for your needs.
我传递给request_adapter的选项不能保证对所有设备都有效,但对大多数设备都有效。如果要获取特定后端的所有适配器,可以使用enumerate_adapters。这将为您提供一个迭代器,您可以循环检查其中一个适配器是否满足您的需要。

1
2
3
4
5
6
7
8
let adapter = instance
.enumerate_adapters(wgpu::BackendBit::PRIMARY)
.filter(|adapter| {
// Check if this adapter supports our surface
adapter.get_swap_chain_preferred_format(&surface).is_some()
})
.first()
.unwrap()

For more fields you can use to refine you’re search check out the docs.
有关可用于优化搜索的更多字段,请查看文档

We need the adapter to create the device and queue.
我们需要适配器来创建设备和队列。

1
2
3
4
5
6
7
8
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
label: None,
},
None, // Trace path
).await.unwrap();

The features field on DeviceDescriptor, allows us to specify what extra features we want. For this simple example, I’ve decided not to use any extra features.
DeviceDescriptor上的features字段允许我们指定所需的额外功能。对于这个简单的例子,我决定不使用任何额外的特性。

The device you have limits the features you can use. If you want to use certain features you may need to limit what devices you support, or provide work arounds.
You can get a list of features supported by your device using adapter.features(), or device.features().
You can view a full list of features here.

您拥有的设备限制了您可以使用的功能。如果您想使用某些功能,您可能需要限制您支持的设备,或提供解决方案。
您可以使用adapter.features()或device.features()获取设备支持的功能列表。
您可以在此处查看功能的完整列表

The limits field describes the limit of certain types of resource we can create. We’ll use the defaults for this tutorial, so we can support most devices. You can view a list of limits here.

limits字段描述了我们可以创建的特定类型资源的限制。本教程将使用默认设置,因此我们可以支持大多数设备。您可以在此处查看limits列表

1
2
3
4
5
6
7
8
let sc_desc = wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
format: adapter.get_swap_chain_preferred_format(&surface).unwrap(),
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
};
let swap_chain = device.create_swap_chain(&surface, &sc_desc);

Here we are defining and creating the swap_chain. The usage field describes how the swap_chain’s underlying textures will be used. RENDER_ATTACHMENT specifies that the textures will be used to write to the screen (we’ll talk about more TextureUsages later).

这里我们定义并创建swap_chain。usage字段描述如何使用swap_chain的基础纹理。RENDER_ATTACHMENT指定纹理将用于写入屏幕(稍后我们将讨论更多纹理图像)。

The format defines how the swap_chains textures will be stored on the gpu. Different displays prefer different formats. We use adapter.get_swap_chain_preferred_format() to figure out the best format to use.

该格式定义如何将swap_chains纹理存储在gpu上。不同的显示器喜欢不同的格式。我们使用adapter.get_swap_chain_preferred_format()来找出最佳的使用格式。

width and height, are the width and height in pixels of the swap chain. This should usually be the width and height of the window.

宽度和高度是swap_chain的宽度和高度(以像素为单位)。这通常应该是窗口的宽度和高度。

The present_mode uses the wgpu::PresentMode enum which determines how to sync the swap chain with the display. You can see all the options in the docs

当前模式使用wgpu::PresentMode枚举,该枚举确定如何将swap_chain与显示同步。您可以在文档中看到所有选项

At the end of the method, we simply return the resulting struct.

在方法的末尾,我们只返回结构体结果。

1
2
3
4
5
6
7
8
9
10
11
        Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
}
}
// ...
}

We’ll want to call this in our main method before we enter the event loop.

在进入事件循环之前,我们希望在main方法中调用它。

1
2
// Since main can't be async, we're going to need to block
let mut state = pollster::block_on(State::new(&window));

You can use heavier libraries like async_std and tokio to make main async, so you can await futures. I’ve elected not to use these crates as this tutorial is not about writing an async application, and the futures created by wgpu do not require special executor support. We just need some way to interact with wgpu’s async functions, and the pollster crate is enough for that.

您可以使用较重的库(如async_std和tokio)来实现main函数异步,因此您可以等待futures。我选择不使用这些crates,因为本教程不是关于编写异步应用程序的,而且wgpu创建的crates不需要特殊的执行器支持。我们只需要一些与wgpu的异步函数交互的方法,the pollster crate就足够了。

resize()

If we want to support resizing in our application, we’re going to need to recreate the swap_chain everytime the window’s size changes. That’s the reason we stored the physical size and the sc_desc used to create the swapchain. With all of these, the resize method is very simple.

如果我们想在应用程序中支持调整大小,我们需要在每次窗口大小更改时重新创建swap_chain。这就是我们存储用于创建swap_chain的物理大小和sc_desc的原因。有了所有这些,调整大小的方法非常简单。

1
2
3
4
5
6
7
8
9
// impl State
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.sc_desc.width = new_size.width;
self.sc_desc.height = new_size.height;
self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc);
}
}

There’s nothing really different here from creating the swap_chain initially, so I won’t get into it.

这里与最初创建swap_chain没有什么不同,所以我就不谈了。

We call this method in main() in the event loop for the following events.

对于以下事件,我们在main函数的事件循环的中调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
match event {
// ...

} if window_id == window.id() => if !state.input(event) {
match event {
// ...

WindowEvent::Resized(physical_size) => {
state.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
// new_inner_size is &&mut so we have to dereference it twice
state.resize(**new_inner_size);
}
// ...
}

input()

input() returns a bool to indicate whether an event has been fully processed. If the method returns true, the main loop won’t process the event any further.

input函数返回一个bool以指示事件是否已被完全处理。如果该方法返回true,则主循环将不再进一步处理该事件。

We’re just going to return false for now because we don’t have any events we want to capture.

我们现在只返回false,因为我们没有任何要捕获的事件。

1
2
3
4
// impl State
fn input(&mut self, event: &WindowEvent) -> bool {
false
}

We need to do a little more work in the event loop. We want State to have priority over main(). Doing that (and previous changes) should have your loop looking like this.

我们需要在事件循环中做更多的工作。我们希望State优先于main()。这样做(以及之前的更改)应该使循环看起来像这样。

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
// main()
event_loop.run(move |event, _, control_flow| {
match event {
Event::WindowEvent {
ref event,
window_id,
} if window_id == window.id() => if !state.input(event) { // UPDATED!
match event {
WindowEvent::CloseRequested
| WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
},
..
} => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(physical_size) => {
state.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
state.resize(**new_inner_size);
}
_ => {}
}
}
_ => {}
}
});

update()

We don’t have anything to update yet, so leave the method empty.

1
2
3
fn update(&mut self) {
// remove `todo!()`
}

render()

Here’s where the magic happens. First we need to get a frame to render to. This will include a wgpu::Texture and wgpu::TextureView that will hold the actual image we’re drawing to (we’ll cover this more when we talk about textures).

这就是魔法发生的地方。首先,我们需要得到一个帧来渲染。这将包括wgpu::Texture和wgpu::TextureView,它们将保存我们正在绘制的实际图像(我们将在讨论纹理时详细介绍这一点)。

1
2
3
4
5
6
7
// impl State

fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
let frame = self
.swap_chain
.get_current_frame()?
.output;

We also need to create a CommandEncoder to create the actual commands to send to the gpu. Most modern graphics frameworks expect commands to be stored in a command buffer before being sent to the gpu. The encoder builds a command buffer that we can then send to the gpu.

我们还需要创建CommandEncoder来创建发送到gpu的实际命令。大多数现代图形框架都希望命令在发送到gpu之前存储在命令缓冲区中。编码器构建一个命令缓冲区,然后我们可以将其发送到gpu。

1
2
3
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});

Now we can actually get to clearing the screen (long time coming). We need to use the encoder to create a RenderPass. The RenderPass has all the methods to do the actual drawing. The code for creating a RenderPass is a bit nested, so I’ll copy it all here, and talk about the pieces.

现在我们可以开始清理屏幕了(很长一段时间就要到了)。我们需要使用编码器来创建一个RenderPass。RenderPass具有进行实际绘制的所有方法。创建RenderPass的代码有点嵌套,所以我将在这里全部复制,并讨论各个部分。

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
    {
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
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,
});
}

// submit will accept anything that implements IntoIter
self.queue.submit(std::iter::once(encoder.finish()));

Ok(())
}

First things first, let’s talk about the {}. encoder.begin_render_pass(…) borrows encoder mutably (aka &mut self). We can’t call encoder.finish() until we release that mutable borrow. The {} around encoder.begin_render_pass(…) tells rust to drop any variables within them when the code leaves that scope thus releasing the mutable borrow on encoder and allowing us to finish() it. If you don’t like the {}, you can also use drop(render_pass) to achieve the same effect.

首先,让我们先谈谈{}包括的代码。 encoder.begin_render_pass(…)使encoder可变(aka &mut self)。在释放该可变借用之前,我们无法调用encoder.finish()。使用{}包括encoder.begin_render_pass(…)告诉rust在代码离开该范围时删除其中的任何变量,从而释放借用的可变编码器,并允许我们完成它。如果不喜欢{},也可以使用drop(render_pass)来实现相同的效果。

We can get the same results by removing the {}, and the let _render_pass = line, but we need access to the _render_pass in the next tutorial, so we’ll leave it as is.

我们可以通过删除{}和let _render_pass=line来获得相同的结果,但是我们需要在下一个教程中访问_render_pass,所以我们将保持原样。

The last lines of the code tell wgpu to finish the command buffer, and to submit it to the gpu’s render queue.

代码的最后几行告诉wgpu完成命令缓冲区,并将其提交到gpu的渲染队列。

We need to update the event loop again to call this method. We’ll also call update before it too.

我们需要再次更新事件循环以调用此方法。我们也会在更新之前调用update。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main()
event_loop.run(move |event, _, control_flow| {
match event {
// ...
Event::RedrawRequested(_) => {
state.update();
match state.render() {
Ok(_) => {}
// Recreate the swap_chain if lost
Err(wgpu::SwapChainError::Lost) => state.resize(state.size),
// The system is out of memory, we should probably quit
Err(wgpu::SwapChainError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
}
Event::MainEventsCleared => {
// RedrawRequested will only trigger once, unless we manually
// request it.
window.request_redraw();
}
// ...
}
});

Wait, what’s going on with RenderPassDescriptor?

Some of you may be able to tell what’s going on just by looking at it, but I’d be remiss if I didn’t go over it. Let’s take a look at the code again.

你们中的一些人可能仅仅通过看它就能知道发生了什么,但如果我不仔细看一下,那我就是失职了。让我们再看一遍代码。

1
2
3
4
5
6
7
&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
// ...
],
depth_stencil_attachment: None,
}

A RenderPassDescriptor only has three fields: label, color_attachments and depth_stencil_attachment. The color_attachements describe where we are going to draw our color to. We’ll use depth_stencil_attachment later, but we’ll set it to None for now.

RenderPassDescriptor只有三个字段:label、color_attachments和depth_stencil_attachment。color_attachments描述了我们将在何处绘制颜色。稍后我们将使用depth_stencil_attachment,但现在将其设置为None。

1
2
3
4
5
6
7
8
9
10
11
12
13
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,
}
}

The RenderPassColorAttachment has the view field which informs wgpu what texture to save the colors to. In this case we specify frame.view that we created using swap_chain.get_current_frame(). This means that any colors we draw to this attachment will get drawn to the screen.

RenderPassColorAttachment有一个视图字段,用于通知wgpu要保存颜色的纹理。在本例中,我们指定使用swap_chain.get_current_frame()创建的frame.view。这意味着我们在此附件中绘制的任何颜色都将被绘制到屏幕上。

The resolve_target is the texture that will receive the resolved output. This will be the same as attachment unless multisampling is enabled. We don’t need to specify this, so we leave it as None.

resolve_target是将接收解析输出的纹理。除非启用了多重采样,否则这将与attachment相同。我们不需要指定它,所以我们将其保留为None。

The ops field takes a wpgu::Operations object. This tells wgpu what to do with the colors on the screen (specified by frame.view). The load field tells wgpu how to handle colors stored from the previous frame. Currently we are clearing the screen with a bluish color.

ops字段接受一个wpgu::Operations对象。这告诉wgpu如何处理屏幕上的颜色(由frame.view指定)。load字段告诉wgpu如何处理从上一帧存储的颜色。目前,我们正在清除带有蓝色的屏幕。

Challenge

Modify the input() method to capture mouse events, and update the clear color using that. Hint: you’ll probably need to use WindowEvent::CursorMoved.

修改input()方法以捕获鼠标事件,并使用该方法更新清除颜色。提示:您可能需要使用WindowEvent::CursorMoved。

Check out the code!

Once a GPUDevice has been obtained during an application initialization routine, we can describe the WebGPU platform as consisting of the following layers:

  1. User agent implementing the specification.
  2. Operating system with low-level native API drivers for this device.
  3. Actual CPU and GPU hardware.

在应用程序初始化例程中获得GPU设备后,我们可以将WebGPU平台描述为由以下层组成:

  1. 实现规范的用户代理。
  2. 此设备具有低级本机API驱动的操作系统。
  3. 实际的CPU和GPU硬件。

Each layer of the WebGPU platform may have different memory types that the user agent needs to consider when implementing the specification:

  • The script-owned memory, such as an ArrayBuffer created by the script, is generally not accessible by a GPU driver.
  • A user agent may have different processes responsible for running the content and communication to the GPU driver. In this case, it uses inter-process shared memory to transfer data.
  • Dedicated GPUs have their own memory with high bandwidth, while integrated GPUs typically share memory with the system.

WebGPU平台的每一层可能有不同的内存类型,用户代理在执行规范时需要考虑:

  • 脚本拥有的内存(如脚本创建的ArrayBuffer)通常不可由GPU驱动程序访问。
  • 用户代理可以具有负责运行内容和与GPU驱动程序通信的不同进程。在这种情况下,它使用进程间共享内存来传输数据。
  • 专用GPU拥有自己的高带宽内存,而集成GPU通常与系统共享内存。

Most physical resources are allocated in the memory of type that is efficient for computation or rendering by the GPU. When the user needs to provide new data to the GPU, the data may first need to cross the process boundary in order to reach the user agent part that communicates with the GPU driver. Then it may need to be made visible to the driver, which sometimes requires a copy into driver-allocated staging memory. Finally, it may need to be transferred to the dedicated GPU memory, potentially changing the internal layout into one that is most efficient for GPUs to operate on.

大多数物理资源都分配在GPU高效计算或渲染的内存中。当用户需要向GPU提供新数据时,数据可能首先需要跨越进程边界以到达与GPU驱动程序通信的用户代理部分。然后可能需要使其对驱动程序可见,这有时需要将副本复制到驱动程序分配的暂存内存中。最后,可能需要将其传输到专用GPU内存,从而可能会将内部布局更改为GPU操作最有效的布局。

All of these transitions are done by the WebGPU implementation of the user agent.

所有这些转换都由用户代理的WebGPU实现完成。

Note: This example describes the worst case, while in practice the implementation may not need to cross the process boundary, or may be able to expose the driver-managed memory directly to the user behind an ArrayBuffer, thus avoiding any data copies.

注: 本例描述了最坏的情况,而实际上实现可能不需要跨越进程边界,或者可能能够将驱动程序管理的内存直接暴露给ArrayBuffer后面的用户,从而避免任何数据拷贝。