use std::{collections::HashSet, time::Instant}; use glam::{mat4, vec2, vec3, vec4, Mat4, Quat, Vec2, Vec3}; use glium::{ backend::{glutin::SimpleWindowBuilder, Facade}, glutin::config::ConfigTemplateBuilder, implement_vertex, index::PrimitiveType, uniform, winit::event::{Event, WindowEvent}, DrawParameters, Program, Surface, VertexBuffer, }; use winit::{ event::ElementState, event_loop::EventLoop, keyboard::{KeyCode, PhysicalKey}, }; mod scene; // The coordinate system: // * X: forward // * Y: left // * Z: up #[derive(Copy, Clone)] struct Vertex { position: [f32; 3], } implement_vertex!(Vertex, position); struct DragCtl { state: S, ctl: Option<(Vec2, S)>, } impl DragCtl { pub fn new(state: S) -> Self { Self { state, ctl: None } } pub fn state(&self) -> S { self.state } pub fn on_button(&mut self, pos: Vec2, state: ElementState) { match state { ElementState::Pressed => { if self.ctl.is_none() { self.ctl = Some((pos, self.state)); } } ElementState::Released => { self.ctl = None; } } } pub fn on_move(&mut self, pos: Vec2, f: impl FnOnce(S, Vec2) -> S) { if let Some((old_pos, old_state)) = self.ctl { self.state = f(old_state, pos - old_pos); } } } struct Wireframe { color: Vec3, mode: PrimitiveType, data: VertexBuffer, } fn prepare_scene(display: &impl Facade) -> Vec { scene::build() .into_iter() .map(|line| { let color = line.color; let mode; let data: Vec; match line.line { scene::Line::Lines(_) => todo!(), scene::Line::Strip(pts) => { mode = PrimitiveType::LineStrip; data = pts .into_iter() .map(|p| Vertex { position: p.to_array(), }) .collect(); } scene::Line::Loop(pts) => { mode = PrimitiveType::LineLoop; data = pts .into_iter() .map(|p| Vertex { position: p.to_array(), }) .collect(); } }; let data = VertexBuffer::new(display, &data).unwrap(); Wireframe { color, mode, data } }) .collect() } fn main() { let event_loop = EventLoop::builder().build().unwrap(); let cfg = ConfigTemplateBuilder::new().with_multisampling(8); let (window, display) = SimpleWindowBuilder::new() .with_config_template_builder(cfg) .with_title("Refraction: Wireframe") .build(&event_loop); let mut keys_pressed = HashSet::::new(); let vs_src = include_str!("ray.v.glsl"); let fs_src = include_str!("ray.f.glsl"); let program = Program::from_source(&display, vs_src, fs_src, None).unwrap(); let scene = prepare_scene(&display); let rot = Quat::from_euler(glam::EulerRot::ZYX, std::f32::consts::FRAC_PI_4, 0., 0.); let mut cur_pos = vec2(0., 0.); let mut cam_pos = rot * vec3(-200., 0., 50.); let mut cam_rot = DragCtl::new(rot); let mut t1 = Instant::now(); #[allow(deprecated)] event_loop .run(move |ev, window_target| match ev { Event::WindowEvent { event, .. } => match event { WindowEvent::RedrawRequested => { let dt = { let t2 = Instant::now(); let dt = t2 - t1; t1 = t2; dt.as_secs_f32() }; let v: Vec3 = { let ctl_left = keys_pressed.contains(&PhysicalKey::Code(KeyCode::KeyA)); let ctl_right = keys_pressed.contains(&PhysicalKey::Code(KeyCode::KeyD)); let ctl_fwd = keys_pressed.contains(&PhysicalKey::Code(KeyCode::KeyW)); let ctl_bwd = keys_pressed.contains(&PhysicalKey::Code(KeyCode::KeyS)); let ctl_up = keys_pressed.contains(&PhysicalKey::Code(KeyCode::Space)); let ctl_down = keys_pressed.contains(&PhysicalKey::Code(KeyCode::ShiftLeft)); [ (ctl_left, vec3(0., 1., 0.)), (ctl_right, vec3(0., -1., 0.)), (ctl_fwd, vec3(1., 0., 0.)), (ctl_bwd, vec3(-1., 0., 0.)), (ctl_up, vec3(0., 0., 1.)), (ctl_down, vec3(0., 0., -1.)), ] .into_iter() .filter_map(|(ctl, dir)| ctl.then_some(dir)) .sum() }; cam_pos += 100. * dt * (cam_rot.state() * v); let size = window.inner_size(); let size = vec2(size.width as f32, size.height as f32).normalize() * std::f32::consts::SQRT_2; let proj = make_proj_matrix(vec3(size.x, size.y, 2.), (1., 4096.)); let my_to_gl = mat4( vec4(0., 0., 1., 0.), vec4(-1., 0., 0., 0.), vec4(0., 1., 0., 0.), vec4(0., 0., 0., 1.), ); let view = my_to_gl * Mat4::from_quat(cam_rot.state().inverse()) * Mat4::from_translation(-cam_pos); let mut target = display.draw(); target.clear_color(0.0, 0.0, 0.0, 0.0); let mvp = proj * view; let params = DrawParameters { blend: glium::Blend { color: glium::BlendingFunction::Addition { source: glium::LinearBlendingFactor::One, destination: glium::LinearBlendingFactor::OneMinusSourceAlpha, }, alpha: glium::BlendingFunction::Addition { source: glium::LinearBlendingFactor::One, destination: glium::LinearBlendingFactor::OneMinusSourceAlpha, }, constant_value: (0., 0., 0., 0.), }, line_width: Some(3.), smooth: Some(glium::Smooth::Nicest), ..Default::default() }; for mesh in &scene { let uniforms = uniform! { mvp: mvp.to_cols_array_2d(), color: mesh.color.to_array(), }; let indices = glium::index::NoIndices(mesh.mode); target .draw(&mesh.data, &indices, &program, &uniforms, ¶ms) .unwrap(); } target.finish().unwrap(); } WindowEvent::MouseInput { device_id: _, state, button, } => match button { winit::event::MouseButton::Right => cam_rot.on_button(cur_pos, state), _ => {} }, WindowEvent::CursorMoved { device_id: _, position, } => { let size = window.inner_size(); let size = vec2(size.width as f32, size.height as f32); cur_pos = vec2(position.x as f32, position.y as f32) / size.length(); cam_rot.on_move(cur_pos, |init, off| { window.request_redraw(); init * Quat::from_euler(glam::EulerRot::ZYX, 2. * off.x, -2. * off.y, 0.) }); } WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _, } => { match event.state { ElementState::Pressed => keys_pressed.insert(event.physical_key), ElementState::Released => keys_pressed.remove(&event.physical_key), }; } WindowEvent::Resized(window_size) => { display.resize(window_size.into()); } WindowEvent::CloseRequested => { window_target.exit(); } _ => (), }, Event::AboutToWait => { window.request_redraw(); } _ => (), }) .unwrap(); } /// Make a projection matrix, assuming input coordinates are (right, up, forward). /// /// `corner` is a vector that will be mapped to (x=1, y=1) after the perspective division. /// `zrange` is the Z range that will be mapped to z∈[-1, 1]. It has no other effect. Both ends have to be positive though. fn make_proj_matrix(corner: Vec3, zrange: (f32, f32)) -> Mat4 { let scale = 1.0 / corner; let zspan = zrange.1 - zrange.0; mat4( scale.x * vec4(1., 0., 0., 0.), scale.y * vec4(0., 1., 0., 0.), scale.z * vec4(0., 0., (zrange.0 + zrange.1) / zspan, 1.), scale.z * vec4(0., 0., -2. * zrange.0 * zrange.1 / zspan, 0.), ) } #[cfg(test)] mod tests { use super::*; use approx::assert_abs_diff_eq; use glam::vec3; #[test] fn test_proj_matrix() { let m = make_proj_matrix(vec3(2., 3., 4.), (0.5, 20.0)); let v = m * vec4(2., 3., 4., 1.); assert_abs_diff_eq!(v.x / v.w, 1.0); assert_abs_diff_eq!(v.y / v.w, 1.0); assert!(-v.w < v.z && v.z < v.w, "z out of range in {v}"); let v = m * vec4(2., 3., 0.5, 1.); assert_abs_diff_eq!(v.x / v.w, 8.0); assert_abs_diff_eq!(v.y / v.w, 8.0); assert_abs_diff_eq!(v.z / v.w, -1.0); let v = m * vec4(2., 3., 20.0, 1.); assert_abs_diff_eq!(v.x / v.w, 0.2); assert_abs_diff_eq!(v.y / v.w, 0.2); assert_abs_diff_eq!(v.z / v.w, 1.0); } }