use egui_fltk_frontend as frontend; use frontend::{ egui, fltk::{ app, enums::Event, prelude::{GroupExt, WidgetBase, WidgetExt, WindowExt}, window, }, pollster, wgpu::{self, util::DeviceExt}, CallbackFn, Painter, RWHandleExt, RenderPass, Timer, }; use std::{borrow::Cow, sync::Arc}; use std::{cell::RefCell, rc::Rc, time::Instant}; fn main() { let fltk_app = app::App::default(); // Initialize fltk windows with minimal size: let mut window = window::GlWindow::default() .with_size(800, 600) .center_screen(); window.set_label("Custom3D Demo Window"); window.make_resizable(true); window.end(); window.show(); window.make_current(); // wgpu::Backends::PRIMARY can be changed accordingly, .e.g: (wgpu::Backends::VULKAN, wgpu::Backends::GL .etc) let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); // let surface = unsafe { instance.create_surface(&window) }; // window.use_compat() for raw-window-handle 4.x compatible let surface = unsafe { instance.create_surface(&window.use_compat()) }; // WGPU 0.11+ support force fallback (if HW implementation not supported), set it to true or false (optional). let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), force_fallback_adapter: false, })) .unwrap(); let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { features: wgpu::Features::default(), limits: wgpu::Limits::default(), label: None, }, None, )) .unwrap(); let texture_format = surface.get_supported_formats(&adapter)[0]; let surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: texture_format, width: window.width() as u32, height: window.height() as u32, present_mode: wgpu::PresentMode::Fifo, }; surface.configure(&device, &surface_config); // Prepare back and front. let render_pass = RenderPass::new(&device, texture_format, 1); let (mut painter, state) = frontend::begin_with(&mut window, render_pass, surface, surface_config); // Create egui state let state = Rc::new(RefCell::new(state)); // Handle window events window.handle({ let state = state.clone(); move |win, ev| match ev { Event::Push | Event::Released | Event::KeyDown | Event::KeyUp | Event::MouseWheel | Event::Resize | Event::Move | Event::Drag | Event::Focus => { // Using "if let ..." for safety. if let Ok(mut state) = state.try_borrow_mut() { state.fuse_input(win, ev); true } else { false } } _ => false, } }); let egui_ctx = egui::Context::default(); let start_time = Instant::now(); // Use Timer for auto repaint if the app is idle. let mut timer = Timer::new(1); let mut custom3d = Custom3d::new(&device, &mut painter).unwrap(); window.draw(move |window| { let mut state = state.borrow_mut(); state.start_time(start_time.elapsed().as_secs_f64()); let app_output = egui_ctx.run(state.take_input(), |ctx| { egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::both() .auto_shrink([false; 2]) .show(ui, |ui| { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("The triangle is being painted using "); ui.hyperlink_to("WGPU", "https://wgpu.rs"); ui.label(" (Portable Rust graphics API awesomeness)"); }); ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui."); egui::Frame::canvas(ui.style()).show(ui, |ui| { custom3d.custom_painting(ui); }); ui.label("Drag to rotate!"); ui.add(egui_demo_lib::egui_github_link_file!()); }); }); }); let window_resized = state.window_resized(); // Make sure to put timer.elapsed() on the last order. if app_output.repaint_after.is_zero() || window_resized || state.mouse_btn_pressed() || timer.elapsed() { state.fuse_output(window, app_output.platform_output); let clipped_primitive = egui_ctx.tessellate(app_output.shapes); let texture = app_output.textures_delta; painter.paint_jobs(&device, &queue, &state.screen_descriptor, clipped_primitive, texture); app::awake(); } }); while fltk_app.wait() { window.flush(); } } const CUSTOM3D_WGPU_SHADER: &str = r#" struct VertexOut { @location(0) color: vec4, @builtin(position) position: vec4, }; struct Uniforms { angle: f32, }; @group(0) @binding(0) var uniforms: Uniforms; var v_positions: array, 3> = array, 3>( vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(-1.0, -1.0), ); var v_colors: array, 3> = array, 3>( vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), ); @vertex fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { var out: VertexOut; out.position = vec4(v_positions[v_idx], 0.0, 1.0); out.position.x = out.position.x * cos(uniforms.angle); out.color = v_colors[v_idx]; return out; } @fragment fn fs_main(in: VertexOut) -> @location(0) vec4 { return in.color; } "#; pub struct Custom3d { angle: f32, } impl Custom3d { pub fn new(device: &wgpu::Device, painter: &mut Painter) -> Option { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(CUSTOM3D_WGPU_SHADER)), }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[], }); let target_texture_format: wgpu::ColorTargetState = painter.surface_config.format.into(); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_main", buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", targets: &[Some(target_texture_format)], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, }); let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: bytemuck::cast_slice(&[0.0]), usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::UNIFORM, }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }], }); // Because the graphics pipeline must have the same lifetime as the egui render pass, // instead of storing the pipeline in our `Custom3D` struct, we insert it into the // `paint_callback_resources` type map, which is stored alongside the render pass. painter .render_pass .paint_callback_resources .insert(TriangleRenderResources { pipeline, bind_group, uniform_buffer, }); Some(Self { angle: 0.0 }) } } impl Custom3d { fn custom_painting(&mut self, ui: &mut egui::Ui) { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); self.angle += response.drag_delta().x * 0.01; // Clone locals so we can move them into the paint callback: let angle = self.angle; // The callback function for WGPU is in two stages: prepare, and paint. // // The prepare callback is called every frame before paint and is given access to the wgpu // Device and Queue, which can be used, for instance, to update buffers and uniforms before // rendering. // // The paint callback is called after prepare and is given access to the render pass, which // can be used to issue draw commands. let cb = CallbackFn::new() .prepare(move |device, queue, paint_callback_resources| { let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); resources.prepare(device, queue, angle); }) .paint(move |_info, rpass, paint_callback_resources| { let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); resources.paint(rpass); }); let callback = egui::PaintCallback { rect, callback: Arc::new(cb), }; ui.painter().add(callback); } } struct TriangleRenderResources { pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, uniform_buffer: wgpu::Buffer, } impl TriangleRenderResources { fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) { // Update our uniform buffer with the angle from the UI queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle])); } fn paint<'rpass>(&'rpass self, rpass: &mut wgpu::RenderPass<'rpass>) { // Draw our triangle! rpass.set_pipeline(&self.pipeline); rpass.set_bind_group(0, &self.bind_group, &[]); rpass.draw(0..3, 0..1); } }