wgpu 简介

wgpu 是一个基于 WebGPU API 的安全且可移植的 Rust 图形库。支持使用 Vulkan、Metal、DirectX 12 和 OpenGL ES 在本地计算机运行,也可以通过 WebGPU 和 WebGL2 的 WebAsssembly 在浏览器上运行。

创建 Rust 项目

如果想要使用 Vulkan 或者 DirectX 12 ,请确保计算机装有对应的驱动程序。

Cargo.toml 依赖:

1
2
3
4
5
6
7
8
9
10
wgpu = "0.17.1"         # WebGPU API 的封装
cgmath = "0.18.0" # 线性代数
env_logger = "0.10.0" # 可以通过环境变量配置日志
futures = "0.3.28" # 类似迭代器接口的 futures 和 streams
gfx-hal = "0.9" # 硬件抽象层
image = "0.24.6" # 过滤器和解码器
log = "0.4.17" # 日志
pollster = "0.3.0" # async 执行器
winit = { version = "0.29.2", features = ["rwh_04", "rwh_05"] } # 跨平台窗口创建
anyhow = "1.0.71" # 提供 anyhow::Error,这是一种基于 trait 对象的错误类型

创建窗口

winit 是一个跨平台的窗口创建和管理库。为了渲染到屏幕上,一般我们还是需要窗口的(处理完直接保存成图片,那就是另外一回事了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::Window,
};

fn main() {
let event_loop = EventLoop::new().unwrap();
let window = Window::new(&event_loop).unwrap();

event_loop.set_control_flow(ControlFlow::Wait);

window.set_title("window");
env_logger::init();

let _ = event_loop.run(move |event, elwt| match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => elwt.exit(),
_ => {}
});
}

这段代码就创建了一个名为 window 的窗口,逻辑也非常简单,只是用来处理事件循环 event_loop

基础的渲染模板

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use wgpu::{IndexFormat, InstanceDescriptor, PrimitiveTopology, ShaderSource};
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::Window,
};

pub struct Inputs<'a> {
pub source: ShaderSource<'a>,
pub topology: PrimitiveTopology,
pub strip_index_format: Option<IndexFormat>,
}

pub async fn run(event_loop: EventLoop<()>, window: Window, inputs: Inputs<'_>, num_vertices: u32) {
let size = window.inner_size();
let instance = wgpu::Instance::new(InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
});

let surface = unsafe { instance.create_surface(&window) }.unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");

let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
},
None,
)
.await
.expect("Failed to create device");

let swapchain_capabilities = surface.get_capabilities(&adapter);
let swapchain_format = swapchain_capabilities.formats[0];

let mut config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: swapchain_capabilities.alpha_modes[0],
view_formats: vec![],
};

surface.configure(&device, &config);

// load the shaders
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: inputs.source,
});

let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[],
push_constant_ranges: &[],
});

let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(swapchain_format.into())],
}),
primitive: wgpu::PrimitiveState {
topology: inputs.topology,
strip_index_format: inputs.strip_index_format,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});

event_loop.set_control_flow(ControlFlow::Wait);
let _ = event_loop.run(move |event, elwt| {
let _ = (&instance, &adapter, &shader, &pipeline_layout);
match event {
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
config.width = size.width;
config.height = size.height;
surface.configure(&device, &config);
}
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
let frame = surface.get_current_texture().unwrap();
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.05,
g: 0.062,
b: 0.08,
a: 1.0,
}),
store: true,
},
})],
depth_stencil_attachment: None,
});
rpass.set_pipeline(&render_pipeline);
rpass.draw(0..num_vertices, 0..1);
}

queue.submit(Some(encoder.finish()));
frame.present();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => elwt.exit(),
_ => {}
}
});
}

这个模板满足了初级阶段需要的过程。首先,得到窗口大小。然后初始化实例,需要注意,这里使用了 Vulkan 作为渲染后端,这个可以根据需要自行选择。

1
2
3
4
5
let size = window.inner_size();
let instance = wgpu::Instance::new(InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
});

(TODO)

第一个 Shader

创建一个 first_triangle.wgsl 的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3> (
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5),
);
return vec4<f32>(pos[in_vertex_index], 0.0, 1.0);
}

@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 0.0, 1.0);
}

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mod common;

use std::borrow::Cow;
use winit::{event_loop::EventLoop, window::Window};

fn main() {
let event_loop = EventLoop::new().unwrap();
let window = Window::new(&event_loop).unwrap();
window.set_title("first_triangle");
env_logger::init();

let inputs = common::Inputs {
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("first_triangle.wgsl"))),
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
};

pollster::block_on(common::run(event_loop, window, inputs, 3));
}

参考资料

  • wgpu 官网
  • Xu, Jack, Practical GPU Graphics with wgpu and Rust: Creating Advanced GPU Graphics on Native Devices and the Web in Rust Using wgpu
  • WGSL 参考文档