Add a custom 3D demo using glow to egui_demo_app (#1546)
This commit is contained in:
parent
bb421c7e8a
commit
3a83a600bb
7 changed files with 287 additions and 72 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1133,6 +1133,7 @@ dependencies = [
|
|||
"egui",
|
||||
"egui_demo_lib",
|
||||
"egui_extras",
|
||||
"egui_glow",
|
||||
"ehttp",
|
||||
"image",
|
||||
"poll-promise",
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
183
egui_demo_app/src/apps/custom3d.rs
Normal file
183
egui_demo_app/src/apps/custom3d.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
Loading…
Reference in a new issue