// Uncomment these following global attributes to silence most warnings of "low" interest: /* #![allow(dead_code)] #![allow(non_snake_case)] #![allow(unreachable_code)] #![allow(unused_mut)] #![allow(unused_unsafe)] #![allow(unused_variables)] */ extern crate nalgebra_glm as glm; use std::sync::{Arc, Mutex, RwLock}; use std::thread; use std::{mem, os::raw::c_void, ptr}; mod shader; mod util; mod mesh; mod scene_graph; use scene_graph::SceneNode; mod toolbox; use toolbox::simple_heading_animation; use glutin::event::{ DeviceEvent, ElementState::{Pressed, Released}, Event, KeyboardInput, VirtualKeyCode::{self, *}, WindowEvent, }; use glutin::event_loop::ControlFlow; // initial window size const INITIAL_SCREEN_W: u32 = 800; const INITIAL_SCREEN_H: u32 = 600; const VERTEX_ATTRIBUTE_INDEX: u32 = 0; const DIMENSIONS: i32 = 3; // == // Helper functions to make interacting with OpenGL a little bit prettier. You *WILL* need these! // == // // Get the size of an arbitrary array of numbers measured in bytes // Example usage: byte_size_of_array(my_array) fn byte_size_of_array(val: &[T]) -> isize { std::mem::size_of_val(&val[..]) as isize } // Get the OpenGL-compatible pointer to an arbitrary array of numbers // Example usage: pointer_to_array(my_array) fn pointer_to_array(val: &[T]) -> *const c_void { &val[0] as *const T as *const c_void } // Get the size of the given type in bytes // Example usage: size_of::() fn size_of() -> i32 { mem::size_of::() as i32 } // Get an offset in bytes for n units of type T, represented as a relative pointer // Example usage: offset::(4) fn offset(n: u32) -> *const c_void { (n * mem::size_of::() as u32) as *const T as *const c_void } // Get a null pointer (equivalent to an offset of 0) // ptr::null() unsafe fn create_vao(vertices: &Vec, normals: &Vec, colors: &Vec, indices: &Vec) -> u32 { let mut vao_id = 0; gl::GenVertexArrays(1, &mut vao_id); gl::BindVertexArray(vao_id); let mut vbo_id = 0; gl::GenBuffers(1, &mut vbo_id); gl::BindBuffer(gl::ARRAY_BUFFER, vbo_id); gl::BufferData( gl::ARRAY_BUFFER, byte_size_of_array(&vertices), vertices.as_ptr() as *const c_void, gl::STATIC_DRAW, ); let mut max_vertex_attribs = 0; gl::GetIntegerv(gl::MAX_VERTEX_ATTRIBS, &mut max_vertex_attribs); println!("max vertex attributes: {max_vertex_attribs}"); gl::VertexAttribPointer( VERTEX_ATTRIBUTE_INDEX, DIMENSIONS, gl::FLOAT, gl::FALSE, 0, std::ptr::null(), ); gl::EnableVertexAttribArray(VERTEX_ATTRIBUTE_INDEX); let mut index_buf_id = 0; gl::GenBuffers(1, &mut index_buf_id); gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, index_buf_id); gl::BufferData( gl::ELEMENT_ARRAY_BUFFER, byte_size_of_array(&indices), indices.as_ptr() as *const c_void, gl::STATIC_DRAW, ); // color buffer let mut cbo_id = 0; gl::GenBuffers(1, &mut cbo_id); gl::BindBuffer(gl::ARRAY_BUFFER, cbo_id); gl::BufferData( gl::ARRAY_BUFFER, byte_size_of_array(&colors), colors.as_ptr() as *const c_void, gl::STATIC_DRAW, ); gl::VertexAttribPointer(1, 4, gl::FLOAT, gl::FALSE, 0, std::ptr::null()); gl::EnableVertexAttribArray(1); // normal buffer let mut nbo_id = 0; gl::GenBuffers(1, &mut nbo_id); gl::BindBuffer(gl::ARRAY_BUFFER, nbo_id); gl::BufferData( gl::ARRAY_BUFFER, byte_size_of_array(&normals), normals.as_ptr() as *const c_void, gl::STATIC_DRAW, ); gl::VertexAttribPointer(2, DIMENSIONS, gl::FLOAT, gl::FALSE, 0, std::ptr::null()); gl::EnableVertexAttribArray(2); vao_id } // Scene graph drawing function unsafe fn draw_scene(node: &scene_graph::SceneNode, transform_loc: i32, normal_loc: i32, view_projection: &glm::Mat4, transform_so_far: &glm::Mat4) { // Build model matrix from node transformations let translation = glm::translation(&node.position); let rot_x = glm::rotation(node.rotation.x, &glm::vec3(1.0, 0.0, 0.0)); let rot_y = glm::rotation(node.rotation.y, &glm::vec3(0.0, 1.0, 0.0)); let rot_z = glm::rotation(node.rotation.z, &glm::vec3(0.0, 0.0, 1.0)); let rotation = rot_z * rot_y * rot_x; let scaling = glm::scaling(&node.scale); let refp = node.reference_point; let to_ref = glm::translation(&refp); let from_ref = glm::translation(&-refp); let model = translation * to_ref * rotation * scaling * from_ref; let new_transform = *transform_so_far * model; // Draw this node if drawable if node.index_count > 0 { let mvp = view_projection * new_transform; gl::UniformMatrix4fv(transform_loc, 1, gl::FALSE, mvp.as_ptr()); // compute normal matrix (inverse‐transpose of model) let nm4 = glm::transpose(&glm::inverse(&new_transform)); let normal_matrix = glm::mat3( nm4[(0,0)], nm4[(0,1)], nm4[(0,2)], nm4[(1,0)], nm4[(1,1)], nm4[(1,2)], nm4[(2,0)], nm4[(2,1)], nm4[(2,2)], ); gl::UniformMatrix3fv(normal_loc, 1, gl::FALSE, normal_matrix.as_ptr()); gl::BindVertexArray(node.vao_id); gl::DrawElements(gl::TRIANGLES, node.index_count, gl::UNSIGNED_INT, ptr::null()); } // Recurse to children for &child in &node.children { draw_scene(&*child, transform_loc, normal_loc, view_projection, &new_transform); } } fn main() { // Set up the necessary objects to deal with windows and event handling let el = glutin::event_loop::EventLoop::new(); let wb = glutin::window::WindowBuilder::new() .with_title("Gloom-rs") .with_resizable(true) .with_inner_size(glutin::dpi::LogicalSize::new( INITIAL_SCREEN_W, INITIAL_SCREEN_H, )); let cb = glutin::ContextBuilder::new().with_vsync(true); let windowed_context = cb.build_windowed(wb, &el).unwrap(); // Uncomment these if you want to use the mouse for controls, but want it to be confined to the screen and/or invisible. // windowed_context.window().set_cursor_grab(true).expect("failed to grab cursor"); // windowed_context.window().set_cursor_visible(false); // Set up a shared vector for keeping track of currently pressed keys let arc_pressed_keys = Arc::new(Mutex::new(Vec::::with_capacity(10))); // Make a reference of this vector to send to the render thread let pressed_keys = Arc::clone(&arc_pressed_keys); // Set up shared tuple for tracking mouse movement between frames let arc_mouse_delta = Arc::new(Mutex::new((0f32, 0f32))); // Make a reference of this tuple to send to the render thread let mouse_delta = Arc::clone(&arc_mouse_delta); // Set up shared tuple for tracking changes to the window size let arc_window_size = Arc::new(Mutex::new((INITIAL_SCREEN_W, INITIAL_SCREEN_H, false))); // Make a reference of this tuple to send to the render thread let window_size = Arc::clone(&arc_window_size); // Spawn a separate thread for rendering, so event handling doesn't block rendering let render_thread = thread::spawn(move || { // Acquire the OpenGL Context and load the function pointers. // This has to be done inside of the rendering thread, because // an active OpenGL context cannot safely traverse a thread boundary let context = unsafe { let c = windowed_context.make_current().unwrap(); gl::load_with(|symbol| c.get_proc_address(symbol) as *const _); c }; let mut window_aspect_ratio = INITIAL_SCREEN_W as f32 / INITIAL_SCREEN_H as f32; // Set up openGL unsafe { gl::Enable(gl::DEPTH_TEST); gl::DepthFunc(gl::LESS); gl::Enable(gl::CULL_FACE); gl::Disable(gl::MULTISAMPLE); gl::Enable(gl::BLEND); gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); gl::Enable(gl::PROGRAM_POINT_SIZE); gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); gl::DebugMessageCallback(Some(util::debug_callback), ptr::null()); // Print some diagnostics println!( "{}: {}", util::get_gl_string(gl::VENDOR), util::get_gl_string(gl::RENDERER) ); println!("OpenGL\t: {}", util::get_gl_string(gl::VERSION)); println!( "GLSL\t: {}", util::get_gl_string(gl::SHADING_LANGUAGE_VERSION) ); } // == // Set up your VAO around here let terrain_mesh = mesh::Terrain::load("resources/lunarsurface.obj"); let terrain_vao = unsafe { create_vao(&terrain_mesh.vertices, &terrain_mesh.normals, &terrain_mesh.colors, &terrain_mesh.indices) }; // helicopter VAOs let helicopter = mesh::Helicopter::load("resources/helicopter.obj"); let heli_body_vao = unsafe { create_vao(&helicopter.body.vertices, &helicopter.body.normals, &helicopter.body.colors, &helicopter.body.indices) }; let heli_door_vao = unsafe { create_vao(&helicopter.door.vertices, &helicopter.door.normals, &helicopter.door.colors, &helicopter.door.indices) }; let heli_main_rotor_vao = unsafe { create_vao(&helicopter.main_rotor.vertices, &helicopter.main_rotor.normals, &helicopter.main_rotor.colors, &helicopter.main_rotor.indices) }; let heli_tail_rotor_vao = unsafe { create_vao(&helicopter.tail_rotor.vertices, &helicopter.tail_rotor.normals, &helicopter.tail_rotor.colors, &helicopter.tail_rotor.indices) }; // Scene Graph setup let mut root_node = SceneNode::new(); let mut terrain_node = SceneNode::from_vao(terrain_vao, terrain_mesh.index_count); root_node.add_child(&*terrain_node); // Instantiate multiple helicopters let heli_count = 5; let mut heli_roots = Vec::new(); for _ in 0..heli_count { let mut root = SceneNode::new(); let body_node = SceneNode::from_vao(heli_body_vao, helicopter.body.index_count); let door_node = SceneNode::from_vao(heli_door_vao, helicopter.door.index_count); let mut main_rotor_node = SceneNode::from_vao(heli_main_rotor_vao, helicopter.main_rotor.index_count); let mut tail_rotor_node = SceneNode::from_vao(heli_tail_rotor_vao, helicopter.tail_rotor.index_count); // Set reference point for tail rotor (pivot around its hub) { let mut tr_pin = tail_rotor_node.as_mut(); unsafe { tr_pin.get_unchecked_mut().reference_point = glm::vec3(0.35, 2.3, 10.4); } } root.add_child(&*body_node); root.add_child(&*door_node); root.add_child(&*main_rotor_node); root.add_child(&*tail_rotor_node); terrain_node.add_child(&*root); heli_roots.push(root); } let vertices = vec![ // triangle 1 -0.8, -0.8, 0.0, -0.2, -0.8, 0.0, -0.5, -0.2, 0.0, // triangle 2 0.2, -0.8, 0.0, 0.8, -0.8, 0.0, 0.5, -0.2, 0.0, // triangle 3 -0.3, 0.2, 0.0, 0.3, 0.2, 0.0, 0.0, 0.8, 0.0, ]; let indices = vec![ 0, 1, 2, 3, 4, 5, 6, 7, 8, ]; let colors = vec![ // triangle 1 colors 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // triangle 2 colors 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, // triangle 3 colors 0.5, 0.5, 0.5, 1.0, 1.0, 0.5, 0.0, 1.0, 0.0, 0.5, 1.0, 1.0, ]; let billboard_vertices = vec![ -0.5, -0.5, 0.0, -0.5, 0.5, 0.0, 0.5, 0.5, 0.0, 0.5, -0.5, 0.0, ]; let billboard_indices = vec![ 0, 1, 2, 0, 2, 3, ]; let billboard_colors = vec![ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, ]; let billboard_vao = unsafe { create_vao(&billboard_vertices, &Vec::new(), &billboard_colors, &billboard_indices) }; // Generate a grid of point‐particles let grid_size = 200; let mut particle_vertices = Vec::with_capacity(grid_size * grid_size * 3); let mut particle_colors = Vec::with_capacity(grid_size * grid_size * 4); for i in 0..grid_size { for j in 0..grid_size { let x = (i as f32 / grid_size as f32 - 0.5) * 20.0; let y = (j as f32 / grid_size as f32 - 0.5) * 20.0; let z = -5.0; particle_vertices.extend_from_slice(&[x, y, z]); particle_colors.extend_from_slice(&[1.0, 1.0, 1.0, 1.0]); } } let particle_vao = unsafe { create_vao(&particle_vertices, &Vec::new(), &particle_colors, &Vec::new()) }; let particle_count = (grid_size * grid_size) as i32; // == // Set up your shaders here // Basic usage of shader helper: // The example code below creates a 'shader' object. // It which contains the field `.program_id` and the method `.activate()`. // The `.` in the path is relative to `Cargo.toml`. // This snippet is not enough to do the exercise, and will need to be modified (outside // of just using the correct path), but it only needs to be called once // TODO: Find out what to do with vertex and fragment shader. let simple_shader = unsafe { shader::ShaderBuilder::new() .attach_file("./shaders/simple.vert") .attach_file("./shaders/simple.frag") .link() }; // get uniform location for the matrix (named `transform` in your vertex shader) let transform_loc = unsafe { simple_shader.get_uniform_location("transform") }; let normal_loc = unsafe { simple_shader.get_uniform_location("normalMatrix") }; // Used to demonstrate keyboard handling for exercise 2. let mut _arbitrary_number = 0.0; // feel free to remove // camera position (x, y, z) and orientation (yaw, pitch) let mut cam_pos: glm::Vec3 = glm::vec3(0.0, 0.0, 0.0); let mut cam_yaw: f32 = 0.0; let mut cam_pitch: f32 = 0.0; // The main rendering loop let first_frame_time = std::time::Instant::now(); let mut previous_frame_time = first_frame_time; loop { // Compute time passed since the previous frame and since the start of the program let now = std::time::Instant::now(); let elapsed = now.duration_since(first_frame_time).as_secs_f32(); let delta_time = now.duration_since(previous_frame_time).as_secs_f32(); previous_frame_time = now; // Handle resize events if let Ok(mut new_size) = window_size.lock() { if new_size.2 { context.resize(glutin::dpi::PhysicalSize::new(new_size.0, new_size.1)); window_aspect_ratio = new_size.0 as f32 / new_size.1 as f32; (*new_size).2 = false; println!("Window was resized to {}x{}", new_size.0, new_size.1); unsafe { gl::Viewport(0, 0, new_size.0 as i32, new_size.1 as i32); } } } // Handle keyboard input for camera movement and rotation if let Ok(keys) = pressed_keys.lock() { // movement speed let speed = 10.0 * delta_time; // camera direction vectors based on yaw & pitch let front = glm::vec3( cam_yaw.sin() * cam_pitch.cos(), cam_pitch.sin(), -cam_yaw.cos() * cam_pitch.cos(), ); let right = glm::normalize(&glm::cross(&front, &glm::vec3(0.0, 1.0, 0.0))); let up_vec = glm::normalize(&glm::cross(&right, &front)); for key in keys.iter() { match key { // Move relative to camera orientation VirtualKeyCode::W => { cam_pos -= front * speed; } VirtualKeyCode::S => { cam_pos += front * speed; } VirtualKeyCode::A => { cam_pos -= right * speed; } VirtualKeyCode::D => { cam_pos += right * speed; } VirtualKeyCode::Space => { cam_pos += up_vec * speed; } VirtualKeyCode::LShift => { cam_pos -= up_vec * speed; } // Rotation: adjust yaw and pitch VirtualKeyCode::Left => { cam_yaw -= delta_time; } VirtualKeyCode::Right => { cam_yaw += delta_time; } VirtualKeyCode::Up => { cam_pitch += delta_time; } VirtualKeyCode::Down => { cam_pitch -= delta_time; } _ => {} } } // clamp pitch to avoid flipping let pitch_limit = std::f32::consts::FRAC_PI_2 - 0.01; cam_pitch = cam_pitch.clamp(-pitch_limit, pitch_limit); } // Handle mouse movement. delta contains the x and y movement of the mouse since last frame in pixels if let Ok(mut delta) = mouse_delta.lock() { // == // Optionally access the accumulated mouse movement between // == // frames here with `delta.0` and `delta.1` *delta = (0.0, 0.0); // reset when done } // == // Please compute camera transforms here (exercise 2 & 3) unsafe { // Clear the color and depth buffers gl::ClearColor(0.035, 0.046, 0.078, 1.0); // night sky gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); // == // Issue the necessary gl:: commands to draw your scene here simple_shader.activate(); // build View matrix: translate and rotate world relative to camera let view_translation: glm::Mat4 = glm::translation(&(-cam_pos)); let view_rot_y: glm::Mat4 = glm::rotation(-cam_yaw, &glm::vec3(0.0, 1.0, 0.0)); let view_rot_x: glm::Mat4 = glm::rotation(-cam_pitch, &glm::vec3(1.0, 0.0, 0.0)); let view: glm::Mat4 = view_rot_x * view_rot_y * view_translation; // build Projection matrix let projection: glm::Mat4 = glm::perspective(window_aspect_ratio, std::f32::consts::PI / 4.0, 1.0, 1000.0); // Animate and update multiple helicopters for (i, root) in heli_roots.iter_mut().enumerate() { let time_offset = elapsed + (i as f32) * 0.5; let heading = simple_heading_animation(time_offset); { let mut pin = root.as_mut(); let node = unsafe { pin.get_unchecked_mut() }; node.position = glm::vec3(heading.x, 0.0, heading.z); node.rotation = glm::vec3(heading.pitch, heading.yaw, heading.roll); } // Update rotors: child index 2 = main rotor, 3 = tail rotor { let mut pin = root.as_mut(); let node = unsafe { pin.get_unchecked_mut() }; unsafe { (&mut (*node.children[2]).rotation).y = elapsed * 10.0; (&mut (*node.children[3]).rotation).x = elapsed * 10.0; } } } // Draw scene via scene graph let view_proj = projection * view; draw_scene(&*root_node, transform_loc, normal_loc, &view_proj, &glm::identity()); } // Display the new color buffer on the display context.swap_buffers().unwrap(); // we use "double buffering" to avoid artifacts } }); // == // // == // From here on down there are only internals. // == // // Keep track of the health of the rendering thread let render_thread_healthy = Arc::new(RwLock::new(true)); let render_thread_watchdog = Arc::clone(&render_thread_healthy); thread::spawn(move || { if !render_thread.join().is_ok() { if let Ok(mut health) = render_thread_watchdog.write() { println!("Render thread panicked!"); *health = false; } } }); // Start the event loop -- This is where window events are initially handled el.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; // Terminate program if render thread panics if let Ok(health) = render_thread_healthy.read() { if *health == false { *control_flow = ControlFlow::Exit; } } match event { Event::WindowEvent { event: WindowEvent::Resized(physical_size), .. } => { println!( "New window size received: {}x{}", physical_size.width, physical_size.height ); if let Ok(mut new_size) = arc_window_size.lock() { *new_size = (physical_size.width, physical_size.height, true); } } Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { *control_flow = ControlFlow::Exit; } // Keep track of currently pressed keys to send to the rendering thread Event::WindowEvent { event: WindowEvent::KeyboardInput { input: KeyboardInput { state: key_state, virtual_keycode: Some(keycode), .. }, .. }, .. } => { if let Ok(mut keys) = arc_pressed_keys.lock() { match key_state { Released => { if keys.contains(&keycode) { let i = keys.iter().position(|&k| k == keycode).unwrap(); keys.remove(i); } } Pressed => { if !keys.contains(&keycode) { keys.push(keycode); } } } } // Handle Escape and Q keys separately match keycode { Escape => { *control_flow = ControlFlow::Exit; } Q => { *control_flow = ControlFlow::Exit; } _ => {} } } Event::DeviceEvent { event: DeviceEvent::MouseMotion { delta }, .. } => { // Accumulate mouse movement if let Ok(mut position) = arc_mouse_delta.lock() { *position = (position.0 + delta.0 as f32, position.1 + delta.1 as f32); } } _ => {} } }); }