Add flycam

This commit is contained in:
mars 2022-04-22 21:42:33 -06:00
parent fa1620ddc0
commit 4313ce8017
6 changed files with 329 additions and 8 deletions

View File

@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
bytemuck = { version = "^1.0", features = ["derive"] }
glam = "0.20"
multimap = "0.8"
pollster = "0.2"
rayon = "1"

231
src/camera.rs Normal file
View File

@ -0,0 +1,231 @@
use glam::{Mat4, Quat, Vec2, Vec3};
use std::time::Instant;
use winit::event::{ElementState, VirtualKeyCode};
use std::f32::consts::LN_2;
pub trait Camera {
fn get_eye(&self) -> [f32; 4];
fn get_vp(&self) -> [[f32; 4]; 4];
}
#[derive(Debug)]
pub struct Flycam {
// input
// currently held keys
is_world_up_pressed: bool,
is_world_down_pressed: bool,
is_cam_up_pressed: bool,
is_cam_down_pressed: bool,
is_forward_pressed: bool,
is_backward_pressed: bool,
is_left_pressed: bool,
is_right_pressed: bool,
// accumulated mouse movement yet to be processed
mouse_dx: f32,
mouse_dy: f32,
// state
// timestamp
last_update: Instant,
// camera orientation
euler_x: f32,
euler_y: f32,
// camera movement state
velocity: Vec3,
position: Vec3,
// constants
// camera movement
turn_sensitivity: f32, // coefficient for mouse_dx/dy -> euler_x/y
thrust_mag: f32, // coefficient for thrust acceleration vector
damping_coeff: f32, // coefficient for damping acceleration vector
// camera frustum
aspect: f32,
fovy: f32,
znear: f32,
zfar: f32,
}
impl Flycam {
/// thrust_speed: top speed when using a single thruster, in units/second
/// duration to halve difference between current and target velocity, in seconds
pub fn new(turn_sensitivity: f32, thrust_speed: f32, damper_half_life: f32) -> Self {
Self {
is_world_up_pressed: false,
is_world_down_pressed: false,
is_cam_up_pressed: false,
is_cam_down_pressed: false,
is_forward_pressed: false,
is_backward_pressed: false,
is_left_pressed: false,
is_right_pressed: false,
mouse_dx: 0.0,
mouse_dy: 0.0,
last_update: Instant::now(),
euler_x: 0.0,
euler_y: 0.0,
velocity: Vec3::new(0.0, 0.0, 0.0),
position: Vec3::new(0.0, 0.5, 1.0),
turn_sensitivity,
thrust_mag: thrust_speed / damper_half_life * LN_2,
damping_coeff: LN_2 / damper_half_life,
aspect: 1.0, // TODO compute from size
fovy: std::f32::consts::FRAC_PI_2,
znear: 0.01,
zfar: 100.0,
}
}
}
impl Flycam {
/// update stored keyboard state for use in update()
pub fn process_keyboard(&mut self, key: VirtualKeyCode, state: ElementState) {
let is_pressed = state == ElementState::Pressed;
match key {
VirtualKeyCode::Space => {
self.is_world_up_pressed = is_pressed;
}
VirtualKeyCode::LShift => {
self.is_world_down_pressed = is_pressed;
}
VirtualKeyCode::Q => {
self.is_cam_down_pressed = is_pressed;
}
VirtualKeyCode::E => {
self.is_cam_up_pressed = is_pressed;
}
VirtualKeyCode::W | VirtualKeyCode::Up => {
self.is_forward_pressed = is_pressed;
}
VirtualKeyCode::A | VirtualKeyCode::Left => {
self.is_left_pressed = is_pressed;
}
VirtualKeyCode::S | VirtualKeyCode::Down => {
self.is_backward_pressed = is_pressed;
}
VirtualKeyCode::D | VirtualKeyCode::Right => {
self.is_right_pressed = is_pressed;
}
_ => {}
}
}
/// update accumulated mouse movement for use in update()
pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) {
self.mouse_dx += mouse_dx as f32;
self.mouse_dy += mouse_dy as f32;
}
pub fn resize(&mut self, width: u32, height: u32) {
self.aspect = (width as f32) / (height as f32);
}
/// apply input and update camera movement
pub fn update(&mut self) {
let dt = self.last_update.elapsed();
self.last_update = Instant::now();
let dt = dt.as_micros() as f32 / 1_000_000.0;
self.update_orientation(dt);
self.update_kinematic(dt);
}
fn update_orientation(&mut self, dt: f32) {
let t = self.turn_sensitivity;
self.euler_x -= t * self.mouse_dy; // mouse +y = 2D plane down = look down = 3d space -x
self.euler_y -= t * self.mouse_dx; // mouse +x = 2D plane right = look to the right = 3d space -y
self.mouse_dx = 0.0;
self.mouse_dy = 0.0;
// Clamp euler_x to [-pi/2, pi/2]
let euler_x_limit = std::f32::consts::FRAC_PI_2;
if self.euler_x < -euler_x_limit {
self.euler_x = -euler_x_limit;
} else if self.euler_x > euler_x_limit {
self.euler_x = euler_x_limit;
}
}
/// update velocity and position from acceleration using forward differences
fn update_kinematic(&mut self, dt: f32) {
let net_acc = self.get_thrust_acc() + self.get_damping_acc();
let delta_vel = net_acc * dt;
self.velocity += delta_vel;
let delta_pos = self.velocity * dt;
self.position += delta_pos;
}
/// use keyboard key pairs to trigger directional thrusters in camera and world coordinates
/// thrust_speed is the max speed (under drag) with a single thruster, but combinations can
/// produce higher speeds (e.g. forward and right, camera down and world down)
fn get_thrust_acc(&self) -> glam::Vec3 {
let axis = Self::key_axis;
let thruster_cam_x = axis(self.is_left_pressed, self.is_right_pressed);
let thruster_cam_y = axis(self.is_cam_down_pressed, self.is_cam_up_pressed);
let thruster_cam_z = -axis(self.is_backward_pressed, self.is_forward_pressed); // forward is -z
let thruster_world_y = axis(self.is_world_down_pressed, self.is_world_up_pressed);
let thrusters_cam = Vec3::new(thruster_cam_x, thruster_cam_y, thruster_cam_z);
let thrusters_world = Vec3::new(0.0, thruster_world_y, 0.0);
let cam_to_world = self.get_orientation();
let thrusters_total = thrusters_world + cam_to_world * thrusters_cam;
self.thrust_mag * thrusters_total
}
/// calculate a damping force (proportional to velocity)
/// the damping coefficient is calculated in the constructor, which is parameterized in terms
/// of more physically meaningful values
fn get_damping_acc(&self) -> glam::Vec3 {
self.damping_coeff * -self.velocity
}
/// a helper function to turn a pair of key states into a sign for thruster direction
fn key_axis(negative: bool, positive: bool) -> f32 {
if negative {
if positive {
0.0 // positive + negative cancel out
} else {
-1.0 // negative only
}
} else {
if positive {
1.0 // positive only
} else {
0.0 // neutral
}
}
}
/// the current camera orientation, which can be seen as a rotation (in quaternion form) from
/// camera axes to world axes
/// glam's YXZ ordering matches the standard roll-pitch-yaw Euler angles
fn get_orientation(&self) -> glam::Quat {
Quat::from_euler(glam::EulerRot::YXZ, self.euler_y, self.euler_x, 0.0)
}
}
impl Camera for Flycam {
fn get_eye(&self) -> [f32; 4] {
self.position.extend(0.0).to_array()
}
fn get_vp(&self) -> [[f32; 4]; 4] {
// view matrix is inverted camera pose (world space to camera space)
let rotation = Mat4::from_quat(self.get_orientation().inverse());
let translation = Mat4::from_translation(-self.position);
let view = rotation * translation;
// perspective projection
let proj = Mat4::perspective_rh_gl(self.fovy, self.aspect, self.znear, self.zfar);
let vp = proj * view;
vp.to_cols_array_2d()
}
}

View File

@ -5,16 +5,30 @@ use rayon::prelude::*;
use std::sync::Arc;
use std::sync::Mutex;
pub mod camera;
pub mod mesh;
pub mod pass;
pub mod phase;
pub mod staging;
use camera::Camera;
use pass::*;
use phase::*;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ViewportUniform {
pub vp: [f32; 16],
pub eye: [f32; 4],
pub vp: [[f32; 4]; 4],
}
impl ViewportUniform {
pub fn from_camera(camera: &impl Camera) -> Self {
Self {
eye: camera.get_eye(),
vp: camera.get_vp(),
}
}
}
impl ViewportUniform {
@ -159,6 +173,7 @@ impl Renderer {
pub fn render(
&mut self,
camera: &impl Camera,
surface: &wgpu::Surface,
format: wgpu::TextureFormat,
) -> Result<(), wgpu::SurfaceError> {
@ -170,6 +185,13 @@ impl Renderer {
let frame_index = self.frame_index;
let frame_data = &self.frame_datas[frame_index];
let viewport_uniform = ViewportUniform::from_camera(camera);
self.queue.write_buffer(
&frame_data.viewport_uniform,
0,
bytemuck::cast_slice(&[viewport_uniform]),
);
let phase_passes = multimap::MultiMap::<Phase, usize>::new();
let phase_passes = std::sync::Mutex::new(phase_passes);

View File

@ -72,13 +72,20 @@ fn main() {
let window = WindowBuilder::new().build(&event_loop).unwrap();
let (mut renderer, mut viewport) = pollster::block_on(make_window_renderer(&window));
let mut camera = cyborg::camera::Flycam::new(0.002, 10.0, 0.25);
let mut is_grabbed = false;
let device = renderer.get_device();
let mesh_pass = pass::mesh::MeshPass::new(device.to_owned(), viewport.config.format);
let layouts = renderer.get_layouts();
let mesh_pass = pass::mesh::MeshPass::new(device.to_owned(), layouts.to_owned(), viewport.config.format);
renderer.add_pass(mesh_pass);
event_loop.run(move |event, _, control_flow| match event {
Event::RedrawRequested(_) => {
match renderer.render(&viewport.surface, viewport.config.format) {
println!("camera: {:#?}", camera);
match renderer.render(&camera, &viewport.surface, viewport.config.format) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => viewport.resize(viewport.size),
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
@ -86,12 +93,51 @@ fn main() {
};
}
Event::MainEventsCleared => {
camera.update();
window.request_redraw();
}
Event::DeviceEvent { ref event, .. } => match event {
DeviceEvent::MouseMotion { delta } => {
if is_grabbed {
camera.process_mouse(delta.0, delta.1);
}
}
_ => {}
},
Event::WindowEvent {
ref event,
window_id,
} if window_id == window.id() => match event {
WindowEvent::KeyboardInput {
input:
KeyboardInput {
virtual_keycode: Some(key),
state,
..
},
..
} => {
if *state == ElementState::Pressed && *key == VirtualKeyCode::Escape {
if is_grabbed {
window.set_cursor_grab(false).unwrap();
window.set_cursor_visible(true);
is_grabbed = false;
}
} else {
camera.process_keyboard(*key, *state);
}
}
WindowEvent::MouseInput {
button: MouseButton::Left,
state: ElementState::Pressed,
..
} => {
if !is_grabbed {
window.set_cursor_grab(true).unwrap();
window.set_cursor_visible(false);
is_grabbed = true;
}
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(physical_size) => {
viewport.resize(*physical_size);

View File

@ -1,5 +1,6 @@
use super::*;
use crate::mesh::*;
use crate::RenderLayouts;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
@ -33,6 +34,7 @@ pub struct FrameData {}
pub struct MeshPass {
device: Arc<wgpu::Device>,
layouts: Arc<RenderLayouts>,
attr_store: Arc<AttrStore>,
mesh_pool: Arc<MeshPool>,
vertex_attr_id: AttrId,
@ -44,7 +46,11 @@ pub struct MeshPass {
}
impl MeshPass {
pub fn new(device: Arc<wgpu::Device>, target_format: wgpu::TextureFormat) -> Self {
pub fn new(
device: Arc<wgpu::Device>,
layouts: Arc<RenderLayouts>,
target_format: wgpu::TextureFormat,
) -> Self {
let attr_store = AttrStore::new();
let mesh_pool = MeshPool::new(device.clone(), attr_store.to_owned());
@ -98,7 +104,7 @@ impl MeshPass {
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
bind_group_layouts: &[&layouts.bind_viewport],
push_constant_ranges: &[],
});
@ -141,6 +147,7 @@ impl MeshPass {
Self {
device,
layouts,
attr_store,
mesh_pool,
index_attr_id,
@ -216,6 +223,7 @@ impl RenderPass for MeshPass {
.collect();
cmds.set_pipeline(pipeline);
cmds.set_bind_group(0, data.bind_viewport, &[]);
for (bindings, instances) in mesh_bindings.iter() {
let vertices_pool = bindings.get(self.vertex_attr_id).unwrap();
@ -235,7 +243,10 @@ impl RenderPass for MeshPass {
let is_end = is_start + indices.count as u32;
cmds.draw_indexed(is_start..is_end, vertices.offset as i32, 0..1);
println!("drew a mesh! {}..{} + {}", is_start, is_end, vertices.offset);
println!(
"drew a mesh! {}..{} + {}",
is_start, is_end, vertices.offset
);
}
}

View File

@ -1,3 +1,8 @@
struct CameraUniform {
eye: vec4<f32>;
vp: mat4x4<f32>;
};
struct VertexInput {
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec3<f32>;
@ -9,15 +14,20 @@ struct VertexOutput {
[[location(1)]] color: vec3<f32>;
};
[[group(0), binding(0)]]
var<uniform> camera: CameraUniform;
[[stage(vertex)]]
fn vs_main(
[[builtin(instance_index)]] mesh_idx: u32,
[[builtin(vertex_index)]] vertex_idx: u32,
vertex: VertexInput,
) -> VertexOutput {
let world_pos = vertex.position;
var out: VertexOutput;
out.clip_position = vec4<f32>(vertex.position, 1.0);
out.position = vertex.position;
out.clip_position = camera.vp * vec4<f32>(world_pos, 1.0);
out.position = world_pos;
out.color = vertex.color;
return out;
}