更好的摄像机
这个问题已经被推迟了一段时间。实现一个虚拟摄像机与正确使用 wgpu 关系不大,但它一直困扰着我,所以现在来实现它吧。
lib.rs
已经堆砌很多代码了,所以我们创建一个 camera.rs
文件来放置摄像机代码。先导入一些要用到的文件:
use winit::event::*;
use winit::dpi::PhysicalPosition;
use instant::Duration;
use std::f32::consts::FRAC_PI_2;
const SAFE_FRAC_PI_2: f32 = FRAC_PI_2 - 0.0001;
在 WASM 中使用 std::time::instant
会导致程序恐慌,所以我们使用 instant 包来替代,在 Cargo.toml
引入此依赖:
instant = "0.1"
虚拟摄像机
接下来,需要创建一个新的 Camera
结构体。我们将使用一个 FPS 风格的摄像机,所以要存储位置(position)、 yaw(偏航,水平旋转)以及 pitch(俯仰,垂直旋转), 定义并实现一个 calc_matrix
函数用于创建视图矩阵:
#[derive(Debug)]
pub struct Camera {
pub position: glam::Vec3,
yaw: f32,
pitch: f32,
}
impl Camera {
pub fn new<V: Into<glam::Vec3>>(position: V, yaw: f32, pitch: f32) -> Self {
Self {
position: position.into(),
yaw,
pitch,
}
}
pub fn calc_matrix(&self) -> glam::Mat4 {
let (sin_pitch, cos_pitch) = self.pitch.sin_cos();
let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
glam::Mat4::look_to_rh(
self.position,
glam::Vec3::new(cos_pitch * cos_yaw, sin_pitch, cos_pitch * sin_yaw).normalize(),
glam::Vec3::Y,
)
}
}
投影
只有在窗口调整大小时,投影(Projection)才真正需要改变,所以我们将投影与摄像机分开,创建一个 Projection
结构体:
pub struct Projection {
aspect: f32,
fovy: f32,
znear: f32,
zfar: f32,
}
impl Projection {
pub fn new(width: u32, height: u32, fovy: f32, znear: f32, zfar: f32) -> Self {
Self {
aspect: width as f32 / height as f32,
fovy: fovy.to_radians(),
znear,
zfar,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
self.aspect = width as f32 / height as f32;
}
pub fn calc_matrix(&self) -> glam::Mat4 {
glam::Mat4::perspective_rh(self.fovy, self.aspect, self.znear, self.zfar)
}
}
有一点需要注意:从 perspective_rh
函数返回的是右手坐标系(right-handed coordinate system)的投影矩阵。也就是说,Z 轴是指向屏幕外的,想让 Z 轴指向屏幕内(也就是左手坐标系的投影矩阵)需要使用 perspective_lh
。
可以这样分辨右手坐标系和左手坐标系的区别:在身体的正前方把你的拇指指向右边代表 X 轴,食指指向上方代表 Y 轴,伸出中指代表 Z 轴。此时在你的右手上,中指应该指是向你自己。而在左手上,应该是指向远方。
摄像机控制器
现在,我们需要一个新的摄像机控制器,在 camera.rs
中添加以下代码:
#[derive(Debug)]
pub struct CameraController {
amount_left: f32,
amount_right: f32,
amount_forward: f32,
amount_backward: f32,
amount_up: f32,
amount_down: f32,
rotate_horizontal: f32,
rotate_vertical: f32,
scroll: f32,
speed: f32,
sensitivity: f32,
}
impl CameraController {
pub fn new(speed: f32, sensitivity: f32) -> Self {
Self {
amount_left: 0.0,
amount_right: 0.0,
amount_forward: 0.0,
amount_backward: 0.0,
amount_up: 0.0,
amount_down: 0.0,
rotate_horizontal: 0.0,
rotate_vertical: 0.0,
scroll: 0.0,
speed,
sensitivity,
}
}
pub fn process_keyboard(&mut self, physical_key: &PhysicalKey, logical_key: &Key, state: ElementState) -> bool {
let amount = if state == ElementState::Pressed {
1.0
} else {
0.0
};
match logical_key {
Key::Named(NamedKey::Space) => {
self.amount_up = amount;
return true;
}
_ => {}
}
match physical_key {
PhysicalKey::Code(KeyCode::ShiftLeft) => {
self.amount_down = amount;
true
}
PhysicalKey::Code(KeyCode::KeyW) | PhysicalKey::Code(KeyCode::ArrowUp) => {
self.amount_forward = amount;
true
}
PhysicalKey::Code(KeyCode::KeyA) | PhysicalKey::Code(KeyCode::ArrowLeft) => {
self.amount_left = amount;
true
}
PhysicalKey::Code(KeyCode::KeyS) | PhysicalKey::Code(KeyCode::ArrowDown) => {
self.amount_backward = amount;
true
}
PhysicalKey::Code(KeyCode::KeyD) | PhysicalKey::Code(KeyCode::ArrowRight) => {
self.amount_right = amount;
true
}
_ => false,
}
}
pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) {
self.rotate_horizontal = mouse_dx as f32;
self.rotate_vertical = mouse_dy as f32;
}
pub fn process_scroll(&mut self, delta: &MouseScrollDelta) {
self.scroll = -match delta {
// 假定一行为 100 个像素,你可以随意修改这个值
MouseScrollDelta::LineDelta(_, scroll) => scroll * 100.0,
MouseScrollDelta::PixelDelta(PhysicalPosition {
y: scroll,
..
}) => *scroll as f32,
};
}
pub fn update_camera(&mut self, camera: &mut Camera, dt: Duration) {
let dt = dt.as_secs_f32();
// 前后左右移动
let (yaw_sin, yaw_cos) = camera.yaw.sin_cos();
let forward = glam::Vec3::new(yaw_cos, 0.0, yaw_sin).normalize();
let right = glam::Vec3::new(-yaw_sin, 0.0, yaw_cos).normalize();
camera.position += forward * (self.amount_forward - self.amount_backward) * self.speed * dt;
camera.position += right * (self.amount_right - self.amount_left) * self.speed * dt;
// 变焦(缩放)
// 注意:这不是一个真实的变焦。
// 通过摄像机的位置变化来模拟变焦,使你更容易靠近想聚焦的物体。
let (pitch_sin, pitch_cos) = camera.pitch.sin_cos();
let scrollward = glam::Vec3::new(pitch_cos * yaw_cos, pitch_sin, pitch_cos * yaw_sin).normalize();
camera.position += scrollward * self.scroll * self.speed * self.sensitivity * dt;
self.scroll = 0.0;
// 由于我们没有使用滚动,所以直接修改 y 坐标来上下移动。
camera.position.y += (self.amount_up - self.amount_down) * self.speed * dt;
// 旋转
camera.yaw += self.rotate_horizontal * self.sensitivity * dt;
camera.pitch += -self.rotate_vertical * self.sensitivity * dt;
// 重置旋转值为 0。没有鼠标移动发生时,摄像机就停止旋转。
self.rotate_horizontal = 0.0;
self.rotate_vertical = 0.0;
// 保持摄像机的角度不要太高/太低。
if camera.pitch < -SAFE_FRAC_PI_2 {
camera.pitch = -SAFE_FRAC_PI_2;
} else if camera.pitch > SAFE_FRAC_PI_2 {
camera.pitch = SAFE_FRAC_PI_2;
}
}
}
清理 lib.rs
首先,我们从 lib.rs
中删除 Camera
、 CameraController
,然后导入 camera.rs
:
mod model;
mod texture;
mod camera; // 新增!
接着更新 update_view_proj
以使用新的 Camera
和 Projection
:
struct CameraUniform {
view_position: [f32; 4],
view_proj: [[f32; 4]; 4],
}
impl CameraUniform {
fn new() -> Self {
Self {
view_position: [0.0; 4],
view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
}
}
// 更新!
fn update_view_proj(&mut self, camera: &camera::Camera, projection: &camera::Projection) {
self.view_position = camera.position.extend(1.0).into();
self.view_proj = (projection.calc_matrix() * camera.calc_matrix()).to_cols_array_2d();
}
}
我们还要修改 WgpuApp
来使用新的 Camera
、CameraProjection
和 Projection
,再添加一个mouse_pressed
字段来存储鼠标是否被按下:
struct WgpuApp {
// ...
camera: camera::Camera, // 更新!
projection: camera::Projection, // 新增!
camera_controller: camera::CameraController, // 更新!
// ...
// 新增!
mouse_pressed: bool,
}
别忘了需要导入 winit::dpi::PhysicalPosition
。
然后更新 new()
函数:
impl WgpuAppAction for WgpuApp {
async fn new(window: Arc<winit::window::Window>) -> Self {
// ...
// 更新!
let camera = camera::Camera::new((0.0, 5.0, 10.0), -90.0, -20.0);
let projection = camera::Projection::new(config.width, config.height, 45.0, 0.1, 100.0);
let camera_controller = camera::CameraController::new(4.0, 0.4);
// ...
camera_uniform.update_view_proj(&camera, &projection); // 更新!
// ...
Self {
// ...
camera,
projection, // 新增!
camera_controller,
// ...
mouse_pressed: false, // 新增!
}
}
}
接着在 resize_surface_if_needed
函数中更新投影矩阵 projection
:
fn resize_surface_if_needed(&mut self) {
// 更新!
self.projection.resize(self.size.width, self.size.height);
// ...
}
事件输入函数也需要被更新。 到目前为止,我们一直在使用 WindowEvent
来控制摄像机,这很有效,但它并不是最好的解决方案。winit 文档告诉我们,操作系统通常会对 CursorMoved
事件的数据进行转换,以实现光标加速等效果。
现在为了解决这个问题,可以修改输入函数函数来处理 DeviceEvent
而不是 WindowEvent
:
// 更新!
// UPDATED!
fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
self.camera_controller.process_keyboard(
&event.physical_key,
&event.logical_key,
event.state,
);
true
}
fn mouse_click(&mut self, state: ElementState, button: MouseButton) -> bool {
if button == MouseButton::Left {
self.mouse_pressed = state == ElementState::Pressed;
true
} else {
false
}
}
fn mouse_wheel(&mut self, delta: MouseScrollDelta, _phase: TouchPhase) -> bool {
self.camera_controller.process_scroll(&delta);
true
}
fn device_input(&mut self, event: &DeviceEvent) -> bool {
if let DeviceEvent::MouseMotion { delta } = event {
if self.mouse_pressed {
self.camera_controller.process_mouse(delta.0, delta.1);
return true;
}
}
false
}
update
函数需要多解释一下:CameraController
上的 update_camera
函数有一个参数 dt
,它是帧之间的时间差(delta time,也可以说是时间间隔),用来辅助实现摄像机的平滑移动,使其不被帧速率所锁定。所以将它作为一个参数传入 update
:
fn update(&mut self, dt: instant::Duration) {
// 更新!
self.camera_controller.update_camera(&mut self.camera, dt);
self.camera_uniform.update_view_proj(&self.camera, &self.projection);
// ..
}
既然如此,我们也用 dt
来平滑光源的旋转:
self.light_uniform.position =
(glam::Quat::from_axis_angle(glam::Vec3::Y, (60.0 * dt.as_secs_f32()).to_radians())
* old_position).into(); // 更新!
让我们在 WgpuAppHandler
的 window_event
函数中来实现 dt
的具体计算:
struct WgpuAppHandler<A: WgpuAppAction> {
// ...
/// 上次执行渲染的时间
last_render_time: instant::Instant,
}
impl<A: WgpuAppAction + 'static> ApplicationHandler for WgpuAppHandler<A> {
// ...
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
match event {
// ...
// 更新!
WindowEvent::RedrawRequested => {
let now = instant::Instant::now();
let dt = now - self.last_render_time;
self.last_render_time = now;
app.update(dt);
// ...
}
_ => {}
}
}
}
现在,我们应该可以自由控制摄像机了: