I'm trying to develop a simple application to create simple objects (like a cube) with OpenGL.
So far I've created a wrapper to OpenGL using the "gl" module to initialize vbos, vaos, programs, shaders etc...
Initially I handled the events I was interested on in the main function, using the "glutin" module. The code looked something like this:
main.rs
fn main() {
let event_loop = EventLoop::new();
let window = WindowBuilder::new().with_title("Rust OpenGL");
let gl_context = ContextBuilder::new()
.with_gl(GlRequest::Specific(Api::OpenGl, (3, 3)))
.build_windowed(window, &event_loop)
.expect("Cannot create windowed context");
let gl_context = unsafe {
gl_context
.make_current()
.expect("Failed to make context current")
};
gl::load_with(|ptr| gl_context.get_proc_address(ptr) as *const _);
let v1 = vec![
Vertex((-0.5, -0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
Vertex((0.5, -0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
Vertex((0.5, 0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
Vertex((-0.5, 0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
];
let front_face = Square::new(&v1);
let v2 = vec![
Vertex((-0.5, -0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
Vertex((0.5, -0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
Vertex((0.5, 0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
Vertex((-0.5, 0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
];
let back_face = Square::new(&v2);
let cube = Cube::from(&[front_face, back_face]);
let mut rotation_angle: Rad<f32> = Deg(0.0).into();
event_loop.run(move |event, target, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::LoopDestroyed => (),
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(physical_size) => gl_context.resize(physical_size),
WindowEvent::KeyboardInput { input, .. } => {
if let Some(VirtualKeyCode::A) = input.virtual_keycode {
match input.state {
ElementState::Pressed => {
rotation_angle -= Deg(0.5).into();
gl_context.window().request_redraw();
},
_ => ()
}
}
if let Some(VirtualKeyCode::D) = input.virtual_keycode {
match input.state {
ElementState::Pressed => {
rotation_angle += Deg(0.5).into();
gl_context.window().request_redraw();
},
_ => ()
}
}
}
_ => (),
},
Event::RedrawRequested(_) => {
println!("{:?}", rotation_angle);
let rotation_matrix_y = Matrix4::from_axis_angle(Vector3::unit_y(), rotation_angle);
let rotation_matrix_x = Matrix4::from_axis_angle(Vector3::unit_x(), Deg(30.0));
let transformation_matrix = rotation_matrix_y * rotation_matrix_x;
let transform_location = cube.gl_wrapper().program.get_uniform_location("Transform").unwrap();
unsafe {
gl::UniformMatrix4fv(transform_location as GLint, 1, gl::FALSE, transformation_matrix.as_ptr());
gl::ClearColor(0.0, 0.0, 0.0, 1.0);
gl::Clear(gl::COLOR_BUFFER_BIT);
Renderer::draw(&cube);
}
gl_context.swap_buffers().unwrap();
}
_ => (),
}
});
}
In this demo, the Renderer
struct was responsible for drawing simple shapes, and by pressing A and D keys I could rotate the cube respectively to the left and to the right; I then modified the code a little, but I'd like to focus on the event handling part.
As you might see, the code is a lot, especially to be all in the same function, the main function. I so thought it would be good to refactor this mess a little, and I came up with this:
main.rs
fn main() {
let event_loop = EventLoop::new();
let mut app = Application::new(&event_loop);
event_loop.run(move |event, _, control_flow| {
// Commenting these out as i'm not sure it works
// app.set_control_flow_reference(control_flow);
// app.set_control_flow(ControlFlow::Poll);
*control_flow = ControlFlow::Poll;
let mut generic_dispatcher = GenericDispatcher::new(&mut app);
generic_dispatcher.handle(event);
});
}
application.rs
#[derive(Debug)]
pub struct Application {
control_flow: ControlFlow,
gl_context: ContextWrapper<PossiblyCurrent, Window>
}
impl Application {
pub fn new(event_loop: &EventLoop<()>) -> Self {
let window_builder = WindowBuilder::new().with_title("Rust OpenGL");
let context = ContextBuilder::new()
.with_gl(GlRequest::Specific(Api::OpenGl, (3, 3)))
.build_windowed(window_builder, event_loop)
.expect("Cannot create windowed context");
let context = unsafe {
context
.make_current()
.expect("Failed to make context current")
};
gl::load_with(|ptr| context.get_proc_address(ptr) as *const _);
unsafe {
info::log_error("loading gl");
}
Self {
control_flow: ControlFlow::default(),
gl_context: context,
}
}
pub fn set_control_flow_reference(&mut self, new_control_flow_reference: &mut ControlFlow) {
let mut control_flow_reference = &mut self.control_flow;
control_flow_reference = new_control_flow_reference;
}
pub fn set_control_flow(&mut self, new_control_flow: ControlFlow) {
let control_flow_reference = &mut self.control_flow;
*control_flow_reference = new_control_flow;
}
pub fn gl_context(&self) -> &ContextWrapper<PossiblyCurrent, Window> {
&self.gl_context
}
}
handler.rs
pub trait Handler<E> {
fn handle(&mut self, item: E);
}
pub trait Dispatcher<'a> {
fn new(application: &'a mut Application) -> Self;
fn application(&'a self) -> &'a Application;
}
generic.rs
pub struct GenericDispatcher<'a> {
application: &'a mut Application,
angle_x: Deg<f32>,
angle_y: Deg<f32>,
cube: Cube
}
// snip the impl for the new associated function
impl<'a> Handler<Event<'_, ()>> for GenericDispatcher<'a> {
fn handle(&mut self, item: Event<'_, ()>) {
match item {
Event::NewEvents(_) => {}
Event::WindowEvent { window_id, event } => {
let mut window_dispatcher = WindowDispatcher::new(self.application);
window_dispatcher.handle(event);
}
Event::DeviceEvent { device_id, event } => {
let mut device_dispatcher = DeviceDispatcher::new(self.application);
device_dispatcher.handle(event);
}
Event::UserEvent(_) => {}
Event::Suspended => {}
Event::Resumed => {}
Event::MainEventsCleared => {
self.application.gl_context().window().request_redraw();
}
Event::RedrawRequested(_) => {
println!("angle_y: {:?}", self.angle_y);
println!("angle_x: {:?}", self.angle_x);
let rotation_matrix_y = Matrix4::from_axis_angle(Vector3::unit_y(), self.angle_y);
let rotation_matrix_x = Matrix4::from_axis_angle(Vector3::unit_x(), self.angle_x);
let model_matrix = rotation_matrix_y * rotation_matrix_x;
let model_location = self.cube.gl_wrapper().program.get_uniform_location("Model").unwrap();
unsafe {
gl::UniformMatrix4fv(model_location as GLint, 1, gl::FALSE, model_matrix.as_ptr());
gl::Enable(gl::DEPTH_TEST);
gl::DepthFunc(gl::LESS);
gl::ClearColor(0.0, 0.0, 0.0, 1.0);
gl::Clear(gl::COLOR_BUFFER_BIT);
gl::Clear(gl::DEPTH_BUFFER_BIT);
Renderer::draw(&self.cube);
}
self.application.gl_context().swap_buffers().unwrap();
}
Event::RedrawEventsCleared => {}
Event::LoopDestroyed => {}
}
}
}
window.rs
pub struct WindowDispatcher<'a> {
application: &'a mut Application
}
impl<'a> Handler<WindowEvent<'_>> for WindowDispatcher<'a> {
fn handle(&mut self, item: WindowEvent<'_>) {
let mut keyboard_dispatcher = KeyboardDispatcher::new(self.application);
match item {
WindowEvent::Resized(physical_size) => {
self.application.gl_context().resize(physical_size);
}
WindowEvent::Moved(_) => {}
WindowEvent::CloseRequested => {
self.application.set_control_flow(ControlFlow::Exit);
}
WindowEvent::Destroyed => {}
WindowEvent::DroppedFile(_) => {}
WindowEvent::HoveredFile(_) => {}
WindowEvent::HoveredFileCancelled => {}
WindowEvent::ReceivedCharacter(_) => {}
WindowEvent::Focused(_) => {}
WindowEvent::KeyboardInput { device_id, input, is_synthetic } => {
keyboard_dispatcher.handle(input);
}
WindowEvent::ModifiersChanged(_) => {}
WindowEvent::Ime(_) => {}
WindowEvent::CursorMoved { device_id, position, modifiers } => {}
WindowEvent::CursorEntered { device_id } => {}
WindowEvent::CursorLeft { device_id } => {}
WindowEvent::MouseWheel { device_id, delta, phase, modifiers } => {}
WindowEvent::MouseInput { device_id, state, button, modifiers } => {}
WindowEvent::TouchpadPressure { device_id, pressure, stage } => {}
WindowEvent::AxisMotion { device_id, axis, value } => {}
WindowEvent::Touch(_) => {}
WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size } => {}
WindowEvent::ThemeChanged(_) => {}
WindowEvent::Occluded(_) => {}
}
}
}
keyboard.rs
pub struct KeyboardDispatcher<'a> {
application: &'a mut Application
}
impl<'a> Handler<KeyboardInput> for KeyboardDispatcher<'a> {
fn handle(&mut self, item: KeyboardInput) {
match item {
KeyboardInput { scancode, state, virtual_keycode, modifiers } => {
match state {
ElementState::Pressed => {
println!("{:?} pressed!", virtual_keycode.unwrap());
}
ElementState::Released => {
println!("{:?} released!", virtual_keycode.unwrap());
}
}
}
}
}
}
When executing the program I only get a white window, nothing gets drawn and no event seems to be catched. I can't even move the window around nor close it, to stop the program I have to force the execution to stop.
I've then tried to comment out the creation of the WindowDispatcher
and DeviceDispatcher
and the call Renderer::draw()
inside the handle
method of the GenericDispatcher
to see if that was the problem:
impl<'a> Handler<Event<'_, ()>> for GenericDispatcher<'a> {
fn handle(&mut self, item: Event<'_, ()>) {
// snip
Event::WindowEvent { window_id, event } => {
// let mut window_dispatcher = WindowDispatcher::new(self.application);
// window_dispatcher.handle(event);
}
Event::DeviceEvent { device_id, event } => {
// let mut device_dispatcher = DeviceDispatcher::new(self.application);
// device_dispatcher.handle(event);
}
// snip
Event::RedrawRequested(_) => {
// snip
unsafe {
gl::UniformMatrix4fv(model_location as GLint, 1, gl::FALSE, model_matrix.as_ptr());
gl::Enable(gl::DEPTH_TEST);
gl::DepthFunc(gl::LESS);
gl::ClearColor(0.0, 0.0, 0.0, 1.0);
gl::Clear(gl::COLOR_BUFFER_BIT);
gl::Clear(gl::DEPTH_BUFFER_BIT);
// Renderer::draw(&self.cube);
}
// snip
}
}
}
But i'm still facing the same issue. Sometimes, without changing the code at all, I get a black window and I get printed the angles, as expected, but then everything freezes once again and I have to stop the execution by force.
How can I fix this?
For anyone wondering, I've managed to solve my problem.
Here's the 5 key steps:
Turns out you need more recent version of the gl
and glutin
crates and you also need the winit
, glutin-winit
and raw-window-handle
crates.
Use a enum for every event your application might need.
pub enum CustomEvent {
Log(String),
// others
}
Use a struct to store useful data (like the proxy used to send data to the application):
use winit::event_loop::EventLoopProxy;
pub struct Dispatcher {
// Make sure to pass as generic the enum created in point 2.
proxy: EventLoopProxy<CustomEvent>,
// others
}
and to implement methods to handle events:
pub fn handle_key_pressed(&mut self, physical_key: PhysicalKey, logical_key: Key) {
// Match physical and logical keys
match logical_key {
Key::Named(named) => match named {
// Implement logic
// Send event to the application
self.proxy.
send_event(CustomEvent::Log("hello".to_string())
.unwrap();
}
}
pub fn handle_mouse_pressed(&self) {
println!("Mouse pressed");
}
// Implement other methods
ApplicationHandler
trait for your applicationDefine an Application
(or whatever you want to call it) struct and implement this trait from winit::application
:
use winit::application::ApplicationHandler;
pub struct Application {
// your fields
}
In particular, pay attention to the user_event
function:
// Once again use as generic your CustomEvent enum
impl ApplicationHandler<CustomEvent> for Application {
// Implement every method
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: CustomEvent) {
// Match events received from the Dispatcher event proxy
match event {
CustomEvent::Log(str) => {
println!("{str}");
}
// Implement your logic
}
}
}
// Initialize your app
let app = Application::new();
// Create the event loop (and for the 3rd time, make sure to use the your CustomEvent enum)
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
// Create the event loop proxy
let proxy = event_loop.create_proxy();
// Create your dispatcher, which will need the proxy
let dispatcher = Dispatcher::new(proxy);
// Feel free to organize your code as you like (e.g. store the dispatcher inside the Application itself)
// Run the application
event_loop.run_app(app).unwrap();
And that's it!
Thanks for the help of everyone.
Please know that this implementation might not be the best one or follow idiomatic Rust, it's just how I've done it.