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 | struct Camera { |
The build_view_projection_matrix is where the magic happens.
build_view_projection_matrix就是魔法发生的地方。
- 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.
- 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.
- 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.
- 视图矩阵将世界移动到摄影机的位置并旋转。它本质上是摄像机变换矩阵的逆矩阵。
- proj矩阵包裹场景以提供深度效果。如果不这样做,近处的对象将与远处的对象大小相同。
- Wgpu中的坐标系基于DirectX和Metal的坐标系。这意味着在标准化设备坐标中,x轴和y轴在-1.0到+1.0的范围内,z轴在0.0到+1.0的范围内。cgmath crate(以及大多数游戏数学库)是为OpenGL的坐标系构建的。该矩阵将缩放并将场景从OpenGL坐标系统转换为WGPU坐标系统。我们将对其进行如下定义。
1 |
|
- 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 | struct State { |
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 | // We need this for Rust to store our data correctly for the shaders |
Now that we have our data structured, let’s make our camera_buffer.
1 | // in new() after creating `camera` |
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 | let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
- We only really need camera information in the vertex shader, as that’s what we’ll use to manipulate our vertices.
- 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.
- 我们只在顶点着色器中需要摄影机信息,因为我们将使用这些信息来操纵顶点。
- dynamic字段指示此缓冲区是否将更改大小。如果我们想在制服中存储一系列的东西,这是很有用的。
Now we can create the actual bind group.
1 | let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { |
Like with our texture, we need to register our camera_bind_group_layout with the render pipeline.
1 | let render_pipeline_layout = device.create_pipeline_layout( |
Now we need to add camera_buffer and camera_bind_group to State
1 | struct State { |
The final thing we need to do before we get into shaders is use the bind group in render().
1 | render_pass.set_pipeline(&self.render_pipeline); |
Using the uniform in the vertex shader
Modify the vertex shader to include the following.
1 | // Vertex shader |
- 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]]
- 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).
- 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.
- 根据WGSL规范,block decorator指示此结构类型表示占用着色器资源接口中单个绑定槽的缓冲区资源的内容。任何用作uniform的结构都必须用[[block]]注释
- 因为我们已经创建了一个新的bind group,所以需要指定在着色器中使用的bind group。编号由渲染管道布局决定。纹理绑定组布局列在第一位,因此它是group(0),摄影机绑定组列在第二位,因此它是group(1)。
- 当涉及到矩阵时,乘法顺序很重要。向量在右边,矩阵按顺序在左边。
A controller for our camera
If you run the code right now, you should get something that looks like this.
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 | struct CameraController { |
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 | struct State { |
We’re finally going to add some code to input() (assuming you haven’t already)!
最后,我们将向input()添加一些代码(假设您还没有这样做)!
1 | fn input(&mut self, event: &WindowEvent) -> bool { |
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缓冲区中的值需要更新。有几种主要的方法可以做到这一点。
- 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.
- 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.
- We can use write_buffer on queue.
- 我们可以创建一个单独的缓冲区,并将其内容复制到我们的相机缓冲区。新缓冲区称为暂存缓冲区。这种方法通常是这样做的,因为它只允许gpu访问主缓冲区(在本例中为camera_缓冲区)的内容。gpu可以做一些速度优化,如果我们可以通过cpu访问缓冲区,它就无法做到。
- 我们可以在缓冲区本身上调用映射方法的map_read_async和map_write_async。这些允许我们直接访问缓冲区的内容,但要求我们处理这些方法的异步问题。这还要求我们的缓冲区使用BufferUsage::MAP_READ读取和BufferUsage::MAP_WRITE。我们这里不讨论它,但是如果您想了解更多,可以在没有窗口教程的情况下查看Wgpu。
- 我们可以在队列上使用write_buffer。
We’re going to use option number 3.
1 | fn update(&mut self) { |
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
让我们的模型独立于相机旋转。提示:您需要另一个矩阵。