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.
This commit is contained in:
Jay Oster 2020-09-18 03:57:28 -07:00 committed by GitHub
parent b6526c27e5
commit 11dca72955
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 333 additions and 0 deletions

View file

@ -25,6 +25,7 @@ Rapidly prototype a simple 2D game, pixel-based animations, software renderers,
- [Conway's Game of Life](./examples/conway) - [Conway's Game of Life](./examples/conway)
- [Custom Shader](./examples/custom-shader) - [Custom Shader](./examples/custom-shader)
- [Dear ImGui example with `winit`](./examples/imgui-winit)
- [Minimal example with SDL2](./examples/minimal-sdl2) - [Minimal example with SDL2](./examples/minimal-sdl2)
- [Minimal example with `winit`](./examples/minimal-winit) - [Minimal example with `winit`](./examples/minimal-winit)
- [Pixel Invaders](./examples/invaders) - [Pixel Invaders](./examples/invaders)

View file

@ -0,0 +1,20 @@
[package]
name = "imgui-winit"
version = "0.1.0"
authors = ["Jay Oster <jay@kodewerx.org>"]
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"

View file

@ -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.

View file

@ -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<imgui::MouseCursor>,
about_open: bool,
}
impl Gui {
/// Create Dear ImGui.
pub(crate) fn new<W: HasRawWindowHandle>(
window: &winit::window::Window,
pixels: &pixels::Pixels<W>,
) -> 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]
}

View file

@ -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);
}
}
}

BIN
img/imgui-winit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB