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_demo_lib",
"egui_extras",
"egui_glow",
"ehttp",
"image",
"poll-promise",

View file

@ -38,6 +38,7 @@ chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
eframe = { version = "0.17.0", path = "../eframe" }
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_glow = { version = "0.17.0", path = "../egui_glow" }
# 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;
#[cfg(feature = "http")]
mod http_app;
pub use custom3d::Custom3d;
pub use fractal_clock::FractalClock;
#[cfg(feature = "http")]
pub use http_app::HttpApp;

View file

@ -1,3 +1,5 @@
use egui_glow::glow;
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EasyMarkApp {
@ -41,6 +43,7 @@ impl eframe::App for FractalClockApp {
});
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
@ -67,71 +70,90 @@ impl eframe::App for ColorTestApp {
// ----------------------------------------------------------------------------
/// All the different demo apps.
/// The state that we persist (serialize).
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Apps {
pub struct State {
demo: DemoApp,
easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
clock: FractalClockApp,
color_test: ColorTestApp,
selected_anchor: String,
backend_panel: super::backend_panel::BackendPanel,
}
impl Apps {
fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn eframe::App)> {
/// Wraps many demo/test apps into one.
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![
("✨ Demos", "demo", &mut self.demo as &mut dyn eframe::App),
(
"✨ Demos",
"demo",
&mut self.state.demo as &mut dyn eframe::App,
),
(
"🖹 EasyMark editor",
"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")]
("⬇ HTTP", "http", &mut self.http as &mut dyn eframe::App),
(
"⬇ HTTP",
"http",
&mut self.state.http as &mut dyn eframe::App,
),
(
"🕑 Fractal 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",
"colors",
&mut self.color_test as &mut dyn eframe::App,
&mut self.state.color_test as &mut dyn eframe::App,
),
]
.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 {
#[cfg(feature = "persistence")]
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 {
@ -141,12 +163,13 @@ impl eframe::App for WrapApp {
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(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() {
self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned();
if self.state.selected_anchor.is_empty() {
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| {
@ -154,11 +177,11 @@ impl eframe::App for WrapApp {
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| {
self.backend_panel.ui(ui, frame);
self.state.backend_panel.ui(ui, frame);
ui.separator();
@ -172,7 +195,7 @@ impl eframe::App for WrapApp {
}
if ui.button("Reset everything").clicked() {
*self = Default::default();
self.state = Default::default();
*ui.ctx().memory() = Default::default();
}
});
@ -181,21 +204,26 @@ impl eframe::App for WrapApp {
let mut found_anchor = false;
for (_name, anchor, app) in self.apps.iter_mut() {
if anchor == self.selected_anchor || ctx.memory().everything_is_visible() {
let selected_anchor = self.state.selected_anchor.clone();
for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame);
found_anchor = true;
}
}
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);
}
fn on_exit(&mut self, gl: &glow::Context) {
self.custom3d.on_exit(gl);
}
}
impl WrapApp {
@ -205,27 +233,29 @@ impl WrapApp {
ui.horizontal_wrapped(|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();
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
.selectable_label(self.selected_anchor == anchor, name)
.selectable_label(selected_anchor == anchor, name)
.clicked()
{
self.selected_anchor = anchor.to_owned();
selected_anchor = anchor.to_owned();
if frame.is_web() {
ui.output().open_url(format!("#{}", anchor));
}
}
}
self.state.selected_anchor = selected_anchor;
ui.with_layout(egui::Layout::right_to_left(), |ui| {
if false {
// TODO: fix the overlap on small screens
if let Some(seconds_since_midnight) = crate::seconds_since_midnight() {
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() {
ui.output().open_url("#clock");
}

View file

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

View file

@ -20,6 +20,27 @@ pub use glow::Context;
const VERT_SRC: &str = include_str!("shader/vertex.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`].
///
/// This is responsible for painting egui and managing egui textures.
@ -56,27 +77,6 @@ pub struct Painter {
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 {
/// Create painter.
///