Add a custom 3D demo using glow to egui_demo_app (#1546)

This commit is contained in:
Emil Ernerfeldt 2022-04-30 12:58:29 +02:00 committed by GitHub
parent bb421c7e8a
commit 3a83a600bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 287 additions and 72 deletions

1
Cargo.lock generated
View file

@ -1133,6 +1133,7 @@ dependencies = [
"egui", "egui",
"egui_demo_lib", "egui_demo_lib",
"egui_extras", "egui_extras",
"egui_glow",
"ehttp", "ehttp",
"image", "image",
"poll-promise", "poll-promise",

View file

@ -38,6 +38,7 @@ chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
eframe = { version = "0.17.0", path = "../eframe" } eframe = { version = "0.17.0", path = "../eframe" }
egui = { version = "0.17.0", path = "../egui", features = ["extra_debug_asserts"] } egui = { version = "0.17.0", path = "../egui", features = ["extra_debug_asserts"] }
egui_demo_lib = { version = "0.17.0", path = "../egui_demo_lib", features = ["chrono"] } egui_demo_lib = { version = "0.17.0", path = "../egui_demo_lib", features = ["chrono"] }
egui_glow = { version = "0.17.0", path = "../egui_glow" }
# Optional dependencies: # Optional dependencies:

View file

@ -0,0 +1,183 @@
use std::sync::Arc;
use egui::mutex::Mutex;
use egui_glow::glow;
pub struct Custom3d {
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
rotating_triangle: Arc<Mutex<RotatingTriangle>>,
angle: f32,
}
impl Custom3d {
pub fn new(gl: &glow::Context) -> Self {
Self {
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl))),
angle: 0.0,
}
}
}
impl eframe::App for Custom3d {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("The triangle is being painted using ");
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
ui.label(" (OpenGL).");
});
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| {
self.custom_painting(ui);
});
ui.label("Drag to rotate!");
ui.add(egui_demo_lib::egui_github_link_file!());
});
}
fn on_exit(&mut self, gl: &glow::Context) {
self.rotating_triangle.lock().destroy(gl);
}
}
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;
let rotating_triangle = self.rotating_triangle.clone();
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(move |_info, render_ctx| {
if let Some(painter) = render_ctx.downcast_ref::<egui_glow::Painter>() {
rotating_triangle.lock().paint(painter.gl(), angle);
} else {
eprintln!("Can't do custom painting because we are not using a glow context");
}
}),
};
ui.painter().add(callback);
}
}
struct RotatingTriangle {
program: glow::Program,
vertex_array: glow::VertexArray,
}
#[allow(unsafe_code)] // we need unsafe code to use glow
impl RotatingTriangle {
fn new(gl: &glow::Context) -> Self {
use glow::HasContext as _;
let shader_version = if cfg!(target_arch = "wasm32") {
"#version 300 es"
} else {
"#version 410"
};
unsafe {
let program = gl.create_program().expect("Cannot create program");
let (vertex_shader_source, fragment_shader_source) = (
r#"
const vec2 verts[3] = vec2[3](
vec2(0.0, 1.0),
vec2(-1.0, -1.0),
vec2(1.0, -1.0)
);
const vec4 colors[3] = vec4[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)
);
out vec4 v_color;
uniform float u_angle;
void main() {
v_color = colors[gl_VertexID];
gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0);
gl_Position.x *= cos(u_angle);
}
"#,
r#"
precision mediump float;
in vec4 v_color;
out vec4 out_color;
void main() {
out_color = v_color;
}
"#,
);
let shader_sources = [
(glow::VERTEX_SHADER, vertex_shader_source),
(glow::FRAGMENT_SHADER, fragment_shader_source),
];
let shaders: Vec<_> = shader_sources
.iter()
.map(|(shader_type, shader_source)| {
let shader = gl
.create_shader(*shader_type)
.expect("Cannot create shader");
gl.shader_source(shader, &format!("{}\n{}", shader_version, shader_source));
gl.compile_shader(shader);
if !gl.get_shader_compile_status(shader) {
panic!("{}", gl.get_shader_info_log(shader));
}
gl.attach_shader(program, shader);
shader
})
.collect();
gl.link_program(program);
if !gl.get_program_link_status(program) {
panic!("{}", gl.get_program_info_log(program));
}
for shader in shaders {
gl.detach_shader(program, shader);
gl.delete_shader(shader);
}
let vertex_array = gl
.create_vertex_array()
.expect("Cannot create vertex array");
Self {
program,
vertex_array,
}
}
}
fn destroy(&self, gl: &glow::Context) {
use glow::HasContext as _;
unsafe {
gl.delete_program(self.program);
gl.delete_vertex_array(self.vertex_array);
}
}
fn paint(&self, gl: &glow::Context, angle: f32) {
use glow::HasContext as _;
unsafe {
gl.use_program(Some(self.program));
gl.uniform_1_f32(
gl.get_uniform_location(self.program, "u_angle").as_ref(),
angle,
);
gl.bind_vertex_array(Some(self.vertex_array));
gl.draw_arrays(glow::TRIANGLES, 0, 3);
}
}
}

View file

@ -1,9 +1,9 @@
mod custom3d;
mod fractal_clock; mod fractal_clock;
#[cfg(feature = "http")] #[cfg(feature = "http")]
mod http_app; mod http_app;
pub use custom3d::Custom3d;
pub use fractal_clock::FractalClock; pub use fractal_clock::FractalClock;
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub use http_app::HttpApp; pub use http_app::HttpApp;

View file

@ -1,3 +1,5 @@
use egui_glow::glow;
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EasyMarkApp { struct EasyMarkApp {
@ -41,6 +43,7 @@ impl eframe::App for FractalClockApp {
}); });
} }
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Default)] #[derive(Default)]
@ -67,71 +70,90 @@ impl eframe::App for ColorTestApp {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// All the different demo apps. /// The state that we persist (serialize).
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct Apps { pub struct State {
demo: DemoApp, demo: DemoApp,
easy_mark_editor: EasyMarkApp, easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")] #[cfg(feature = "http")]
http: crate::apps::HttpApp, http: crate::apps::HttpApp,
clock: FractalClockApp, clock: FractalClockApp,
color_test: ColorTestApp, color_test: ColorTestApp,
selected_anchor: String,
backend_panel: super::backend_panel::BackendPanel,
} }
impl Apps { /// Wraps many demo/test apps into one.
fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn eframe::App)> { pub struct WrapApp {
state: State,
// not serialized (because it contains OpenGL buffers etc)
custom3d: crate::apps::Custom3d,
dropped_files: Vec<egui::DroppedFile>,
}
impl WrapApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
let mut slf = Self {
state: State::default(),
custom3d: crate::apps::Custom3d::new(&cc.gl),
dropped_files: Default::default(),
};
#[cfg(feature = "persistence")]
if let Some(storage) = cc.storage {
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
slf.state = state;
}
}
slf
}
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn eframe::App)> {
vec![ vec![
("✨ Demos", "demo", &mut self.demo as &mut dyn eframe::App), (
"✨ Demos",
"demo",
&mut self.state.demo as &mut dyn eframe::App,
),
( (
"🖹 EasyMark editor", "🖹 EasyMark editor",
"easymark", "easymark",
&mut self.easy_mark_editor as &mut dyn eframe::App, &mut self.state.easy_mark_editor as &mut dyn eframe::App,
), ),
#[cfg(feature = "http")] #[cfg(feature = "http")]
("⬇ HTTP", "http", &mut self.http as &mut dyn eframe::App), (
"⬇ HTTP",
"http",
&mut self.state.http as &mut dyn eframe::App,
),
( (
"🕑 Fractal Clock", "🕑 Fractal Clock",
"clock", "clock",
&mut self.clock as &mut dyn eframe::App, &mut self.state.clock as &mut dyn eframe::App,
),
(
"🔺 3D painting",
"custom3e",
&mut self.custom3d as &mut dyn eframe::App,
), ),
( (
"🎨 Color test", "🎨 Color test",
"colors", "colors",
&mut self.color_test as &mut dyn eframe::App, &mut self.state.color_test as &mut dyn eframe::App,
), ),
] ]
.into_iter() .into_iter()
} }
} }
/// Wraps many demo/test apps into one.
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct WrapApp {
selected_anchor: String,
apps: Apps,
backend_panel: super::backend_panel::BackendPanel,
#[cfg_attr(feature = "serde", serde(skip))]
dropped_files: Vec<egui::DroppedFile>,
}
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
#[cfg(feature = "persistence")]
if let Some(storage) = _cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
Self::default()
}
}
impl eframe::App for WrapApp { impl eframe::App for WrapApp {
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self); eframe::set_value(storage, eframe::APP_KEY, &self.state);
} }
fn clear_color(&self) -> egui::Rgba { fn clear_color(&self) -> egui::Rgba {
@ -141,12 +163,13 @@ impl eframe::App for WrapApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if let Some(web_info) = frame.info().web_info.as_ref() { if let Some(web_info) = frame.info().web_info.as_ref() {
if let Some(anchor) = web_info.location.hash.strip_prefix('#') { if let Some(anchor) = web_info.location.hash.strip_prefix('#') {
self.selected_anchor = anchor.to_owned(); self.state.selected_anchor = anchor.to_owned();
} }
} }
if self.selected_anchor.is_empty() { if self.state.selected_anchor.is_empty() {
self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned(); let selected_anchor = self.apps_iter_mut().next().unwrap().0.to_owned();
self.state.selected_anchor = selected_anchor;
} }
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
@ -154,11 +177,11 @@ impl eframe::App for WrapApp {
self.bar_contents(ui, frame); self.bar_contents(ui, frame);
}); });
self.backend_panel.update(ctx, frame); self.state.backend_panel.update(ctx, frame);
if self.backend_panel.open || ctx.memory().everything_is_visible() { if self.state.backend_panel.open || ctx.memory().everything_is_visible() {
egui::SidePanel::left("backend_panel").show(ctx, |ui| { egui::SidePanel::left("backend_panel").show(ctx, |ui| {
self.backend_panel.ui(ui, frame); self.state.backend_panel.ui(ui, frame);
ui.separator(); ui.separator();
@ -172,7 +195,7 @@ impl eframe::App for WrapApp {
} }
if ui.button("Reset everything").clicked() { if ui.button("Reset everything").clicked() {
*self = Default::default(); self.state = Default::default();
*ui.ctx().memory() = Default::default(); *ui.ctx().memory() = Default::default();
} }
}); });
@ -181,21 +204,26 @@ impl eframe::App for WrapApp {
let mut found_anchor = false; let mut found_anchor = false;
for (_name, anchor, app) in self.apps.iter_mut() { let selected_anchor = self.state.selected_anchor.clone();
if anchor == self.selected_anchor || ctx.memory().everything_is_visible() { for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame); app.update(ctx, frame);
found_anchor = true; found_anchor = true;
} }
} }
if !found_anchor { if !found_anchor {
self.selected_anchor = "demo".into(); self.state.selected_anchor = "demo".into();
} }
self.backend_panel.end_of_frame(ctx); self.state.backend_panel.end_of_frame(ctx);
self.ui_file_drag_and_drop(ctx); self.ui_file_drag_and_drop(ctx);
} }
fn on_exit(&mut self, gl: &glow::Context) {
self.custom3d.on_exit(gl);
}
} }
impl WrapApp { impl WrapApp {
@ -205,27 +233,29 @@ impl WrapApp {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
egui::widgets::global_dark_light_mode_switch(ui); egui::widgets::global_dark_light_mode_switch(ui);
ui.checkbox(&mut self.backend_panel.open, "💻 Backend"); ui.checkbox(&mut self.state.backend_panel.open, "💻 Backend");
ui.separator(); ui.separator();
for (name, anchor, _app) in self.apps.iter_mut() { let mut selected_anchor = self.state.selected_anchor.clone();
for (name, anchor, _app) in self.apps_iter_mut() {
if ui if ui
.selectable_label(self.selected_anchor == anchor, name) .selectable_label(selected_anchor == anchor, name)
.clicked() .clicked()
{ {
self.selected_anchor = anchor.to_owned(); selected_anchor = anchor.to_owned();
if frame.is_web() { if frame.is_web() {
ui.output().open_url(format!("#{}", anchor)); ui.output().open_url(format!("#{}", anchor));
} }
} }
} }
self.state.selected_anchor = selected_anchor;
ui.with_layout(egui::Layout::right_to_left(), |ui| { ui.with_layout(egui::Layout::right_to_left(), |ui| {
if false { if false {
// TODO: fix the overlap on small screens // TODO: fix the overlap on small screens
if let Some(seconds_since_midnight) = crate::seconds_since_midnight() { if let Some(seconds_since_midnight) = crate::seconds_since_midnight() {
if clock_button(ui, seconds_since_midnight).clicked() { if clock_button(ui, seconds_since_midnight).clicked() {
self.selected_anchor = "clock".to_owned(); self.state.selected_anchor = "clock".to_owned();
if frame.is_web() { if frame.is_web() {
ui.output().open_url("#clock"); ui.output().open_url("#clock");
} }

View file

@ -53,7 +53,7 @@ impl super::Demo for WidgetGallery {
egui::Window::new(self.name()) egui::Window::new(self.name())
.open(open) .open(open)
.resizable(true) .resizable(true)
.default_width(300.0) .default_width(280.0)
.show(ctx, |ui| { .show(ctx, |ui| {
use super::View as _; use super::View as _;
self.ui(ui); self.ui(ui);

View file

@ -20,6 +20,27 @@ pub use glow::Context;
const VERT_SRC: &str = include_str!("shader/vertex.glsl"); const VERT_SRC: &str = include_str!("shader/vertex.glsl");
const FRAG_SRC: &str = include_str!("shader/fragment.glsl"); const FRAG_SRC: &str = include_str!("shader/fragment.glsl");
#[derive(Copy, Clone)]
pub enum TextureFilter {
Linear,
Nearest,
}
impl Default for TextureFilter {
fn default() -> Self {
TextureFilter::Linear
}
}
impl TextureFilter {
pub(crate) fn glow_code(&self) -> u32 {
match self {
TextureFilter::Linear => glow::LINEAR,
TextureFilter::Nearest => glow::NEAREST,
}
}
}
/// An OpenGL painter using [`glow`]. /// An OpenGL painter using [`glow`].
/// ///
/// This is responsible for painting egui and managing egui textures. /// This is responsible for painting egui and managing egui textures.
@ -56,27 +77,6 @@ pub struct Painter {
destroyed: bool, destroyed: bool,
} }
#[derive(Copy, Clone)]
pub enum TextureFilter {
Linear,
Nearest,
}
impl Default for TextureFilter {
fn default() -> Self {
TextureFilter::Linear
}
}
impl TextureFilter {
pub(crate) fn glow_code(&self) -> u32 {
match self {
TextureFilter::Linear => glow::LINEAR,
TextureFilter::Nearest => glow::NEAREST,
}
}
}
impl Painter { impl Painter {
/// Create painter. /// Create painter.
/// ///