From 11dca72955088ead18a90136814799655d14a54d Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Fri, 18 Sep 2020 03:57:28 -0700 Subject: [PATCH] Add Dear ImGui example (#116) * Add Dear ImGui example - Closes #90 * Change argument order to match `render_with` * Remove unnecessary borrow * Refactor error messages * Add a space * Refactor Gui field privacy * Add a menu bar and allow the about window to be closed - The local bool is necessary because the menu bar closures are not allowed to borrow `self` for mutable access while `imgui::Ui<'ui>` is alive. - The token-based menu bar lifetime is even more verbose than this. --- README.md | 1 + examples/imgui-winit/Cargo.toml | 20 +++++ examples/imgui-winit/README.md | 15 ++++ examples/imgui-winit/src/gui.rs | 150 +++++++++++++++++++++++++++++++ examples/imgui-winit/src/main.rs | 147 ++++++++++++++++++++++++++++++ img/imgui-winit.png | Bin 0 -> 21476 bytes 6 files changed, 333 insertions(+) create mode 100644 examples/imgui-winit/Cargo.toml create mode 100644 examples/imgui-winit/README.md create mode 100644 examples/imgui-winit/src/gui.rs create mode 100644 examples/imgui-winit/src/main.rs create mode 100644 img/imgui-winit.png diff --git a/README.md b/README.md index ab6abbd7..838bc91d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Rapidly prototype a simple 2D game, pixel-based animations, software renderers, - [Conway's Game of Life](./examples/conway) - [Custom Shader](./examples/custom-shader) +- [Dear ImGui example with `winit`](./examples/imgui-winit) - [Minimal example with SDL2](./examples/minimal-sdl2) - [Minimal example with `winit`](./examples/minimal-winit) - [Pixel Invaders](./examples/invaders) diff --git a/examples/imgui-winit/Cargo.toml b/examples/imgui-winit/Cargo.toml new file mode 100644 index 00000000..ce4a02bc --- /dev/null +++ b/examples/imgui-winit/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "imgui-winit" +version = "0.1.0" +authors = ["Jay Oster "] +edition = "2018" +publish = false + +[features] +optimize = ["log/release_max_level_warn"] +default = ["optimize"] + +[dependencies] +env_logger = "0.7" +imgui = "0.4" +imgui-wgpu = "0.9" +imgui-winit-support = { version = "0.4", default-features = false, features = ["winit-22"] } +log = "0.4" +pixels = { path = "../.." } +winit = "0.22" +winit_input_helper = "0.6" diff --git a/examples/imgui-winit/README.md b/examples/imgui-winit/README.md new file mode 100644 index 00000000..4d6ac641 --- /dev/null +++ b/examples/imgui-winit/README.md @@ -0,0 +1,15 @@ +# Dear-ImGui Example + +![Dear-ImGui Example](../../img/imgui-winit.png) + +Minimal example with `imgui` and `winit`. + +## Running + +```bash +cargo run --release --package imgui-winit +``` + +## About + +This example is based on `minimal-winit`, and extends it with `imgui` to render custom GUI elements over your pixel frame buffer. diff --git a/examples/imgui-winit/src/gui.rs b/examples/imgui-winit/src/gui.rs new file mode 100644 index 00000000..4b74e08a --- /dev/null +++ b/examples/imgui-winit/src/gui.rs @@ -0,0 +1,150 @@ +use pixels::{raw_window_handle::HasRawWindowHandle, wgpu, PixelsContext}; +use std::time::Instant; + +/// Manages all state required for rendering Dear ImGui over `Pixels`. +pub(crate) struct Gui { + imgui: imgui::Context, + platform: imgui_winit_support::WinitPlatform, + renderer: imgui_wgpu::Renderer, + last_frame: Instant, + last_cursor: Option, + about_open: bool, +} + +impl Gui { + /// Create Dear ImGui. + pub(crate) fn new( + window: &winit::window::Window, + pixels: &pixels::Pixels, + ) -> Self { + // Create Dear ImGui context + let mut imgui = imgui::Context::create(); + imgui.set_ini_filename(None); + + // Initialize winit platform support + let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui); + platform.attach_window( + imgui.io_mut(), + &window, + imgui_winit_support::HiDpiMode::Default, + ); + + // Configure Dear ImGui fonts + let hidpi_factor = window.scale_factor(); + let font_size = (13.0 * hidpi_factor) as f32; + imgui.io_mut().font_global_scale = (1.0 / hidpi_factor) as f32; + imgui + .fonts() + .add_font(&[imgui::FontSource::DefaultFontData { + config: Some(imgui::FontConfig { + oversample_h: 1, + pixel_snap_h: true, + size_pixels: font_size, + ..Default::default() + }), + }]); + + // Fix incorrect colors with sRGB framebuffer + let style = imgui.style_mut(); + for color in 0..style.colors.len() { + style.colors[color] = gamma_to_linear(style.colors[color]); + } + + // Create Dear ImGui WGPU renderer + let device = pixels.device(); + let queue = pixels.queue(); + let texture_format = wgpu::TextureFormat::Bgra8UnormSrgb; + let renderer = imgui_wgpu::Renderer::new(&mut imgui, &device, &queue, texture_format); + + // Return GUI context + Self { + imgui, + platform, + renderer, + last_frame: Instant::now(), + last_cursor: None, + about_open: true, + } + } + + /// Prepare Dear ImGui. + pub(crate) fn prepare( + &mut self, + window: &winit::window::Window, + ) -> Result<(), winit::error::ExternalError> { + // Prepare Dear ImGui + let io = self.imgui.io_mut(); + self.last_frame = io.update_delta_time(self.last_frame); + self.platform.prepare_frame(io, window) + } + + /// Render Dear ImGui. + pub(crate) fn render( + &mut self, + window: &winit::window::Window, + encoder: &mut wgpu::CommandEncoder, + render_target: &wgpu::TextureView, + context: &PixelsContext, + ) -> imgui_wgpu::RendererResult<()> { + // Start a new Dear ImGui frame and update the cursor + let ui = self.imgui.frame(); + + let mouse_cursor = ui.mouse_cursor(); + if self.last_cursor != mouse_cursor { + self.last_cursor = mouse_cursor; + self.platform.prepare_render(&ui, window); + } + + // Draw windows and GUI elements here + let mut about_open = false; + ui.main_menu_bar(|| { + ui.menu(imgui::im_str!("Help"), true, || { + about_open = imgui::MenuItem::new(imgui::im_str!("About...")).build(&ui); + }); + }); + if about_open { + self.about_open = true; + } + + if self.about_open { + ui.show_about_window(&mut self.about_open); + } + + // Render Dear ImGui with WGPU + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: render_target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + self.renderer + .render(ui.render(), &context.queue, &context.device, &mut rpass) + } + + /// Handle any outstanding events. + pub(crate) fn handle_event( + &mut self, + window: &winit::window::Window, + event: &winit::event::Event<()>, + ) { + self.platform + .handle_event(self.imgui.io_mut(), window, event); + } +} + +fn gamma_to_linear(color: [f32; 4]) -> [f32; 4] { + const GAMMA: f32 = 2.2; + + let x = color[0].powf(GAMMA); + let y = color[1].powf(GAMMA); + let z = color[2].powf(GAMMA); + let w = 1.0 - (1.0 - color[3]).powf(GAMMA); + + [x, y, z, w] +} diff --git a/examples/imgui-winit/src/main.rs b/examples/imgui-winit/src/main.rs new file mode 100644 index 00000000..e6032bfe --- /dev/null +++ b/examples/imgui-winit/src/main.rs @@ -0,0 +1,147 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use crate::gui::Gui; +use log::error; +use pixels::{Error, Pixels, SurfaceTexture}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +mod gui; + +const WIDTH: u32 = 640; +const HEIGHT: u32 = 480; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + box_x: i16, + box_y: i16, + velocity_x: i16, + velocity_y: i16, +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels + Dear ImGui") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .unwrap() + }; + + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + Pixels::new(WIDTH, HEIGHT, surface_texture)? + }; + let mut world = World::new(); + + // Set up Dear ImGui + let mut gui = Gui::new(&window, &pixels); + + event_loop.run(move |event, _, control_flow| { + // Draw the current frame + if let Event::RedrawRequested(_) = event { + // Draw the world + world.draw(pixels.get_frame()); + + // Prepare Dear ImGui + gui.prepare(&window).expect("gui.prepare() failed"); + + // Render everything together + let render_result = pixels.render_with(|encoder, render_target, context| { + // Render the world texture + context.scaling_renderer.render(encoder, render_target); + + // Render Dear ImGui + gui.render(&window, encoder, render_target, context) + .expect("gui.render() failed"); + }); + + // Basic error handling + if render_result + .map_err(|e| error!("pixels.render() failed: {}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + } + + // Handle input events + gui.handle_event(&window, &event); + if input.update(event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + + // Resize the window + if let Some(size) = input.window_resized() { + pixels.resize(size.width, size.height); + } + + // Update internal state and request a redraw + world.update(); + window.request_redraw(); + } + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + box_x: 24, + box_y: 16, + velocity_x: 1, + velocity_y: 1, + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self) { + if self.box_x <= 0 || self.box_x + BOX_SIZE > WIDTH as i16 { + self.velocity_x *= -1; + } + if self.box_y <= 0 || self.box_y + BOX_SIZE > HEIGHT as i16 { + self.velocity_y *= -1; + } + + self.box_x += self.velocity_x; + self.box_y += self.velocity_y; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: [`wgpu::TextureFormat::Rgba8UnormSrgb`] + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let inside_the_box = x >= self.box_x + && x < self.box_x + BOX_SIZE + && y >= self.box_y + && y < self.box_y + BOX_SIZE; + + let rgba = if inside_the_box { + [0x5e, 0x48, 0xe8, 0xff] + } else { + [0x48, 0xb2, 0xe8, 0xff] + }; + + pixel.copy_from_slice(&rgba); + } + } +} diff --git a/img/imgui-winit.png b/img/imgui-winit.png new file mode 100644 index 0000000000000000000000000000000000000000..c3620c812f00ac3a10b10177815be38a0f6f3eab GIT binary patch literal 21476 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 literal 0 HcmV?d00001