From c63bdeab67846db71ce74512d236427c45ef6286 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 23 Mar 2022 11:06:33 +0100 Subject: [PATCH] Add an example of showing 3D using three-d (#1407) --- Cargo.lock | 96 +++++++++++ README.md | 18 +- eframe/Cargo.toml | 1 + .../{custom_3d.rs => custom_3d_glow.rs} | 7 +- eframe/examples/custom_3d_three-d.rs | 156 ++++++++++++++++++ 5 files changed, 274 insertions(+), 4 deletions(-) rename eframe/examples/{custom_3d.rs => custom_3d_glow.rs} (96%) create mode 100644 eframe/examples/custom_3d_three-d.rs diff --git a/Cargo.lock b/Cargo.lock index a7f5b1e1..2d4bb847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -442,6 +451,16 @@ dependencies = [ "libc", ] +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "chrono" version = "0.4.19" @@ -995,6 +1014,7 @@ dependencies = [ "image", "poll-promise", "rfd", + "three-d", ] [[package]] @@ -1486,6 +1506,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gloo-timers" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "glow" version = "0.11.2" @@ -1604,6 +1634,11 @@ name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +dependencies = [ + "num-traits", + "serde", + "zerocopy", +] [[package]] name = "hashbrown" @@ -1807,6 +1842,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" + [[package]] name = "line-wrap" version = "0.1.1" @@ -2122,6 +2163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3038,6 +3080,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "syntect" version = "4.6.0" @@ -3126,6 +3180,25 @@ dependencies = [ "once_cell", ] +[[package]] +name = "three-d" +version = "0.11.0" +source = "git+https://github.com/asny/three-d?rev=fa475673e284e05b2f4e068769dce3ec5bcabc8d#fa475673e284e05b2f4e068769dce3ec5bcabc8d" +dependencies = [ + "cgmath", + "gl_generator", + "gloo-timers", + "glow", + "half", + "js-sys", + "log", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "time" version = "0.1.43" @@ -3476,6 +3549,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if 1.0.0", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -3950,6 +4025,27 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + [[package]] name = "zvariant" version = "3.1.2" diff --git a/README.md b/README.md index f8c4ef3a..be9e8663 100644 --- a/README.md +++ b/README.md @@ -338,9 +338,23 @@ On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and To alleviate the above mentioned performance issues the default max-width of an egui web app is 1024 points. You can change this by overriding the `fn max_size_points` of [`epi::App`](https://docs.rs/epi/latest/epi/trait.App.html). ### How do I render 3D stuff in an egui area? -egui can't do 3D graphics itself, but if you use a 3D library (e.g. [`glium`](https://github.com/glium/glium) using [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium), or [`miniquad`](https://github.com/not-fl3/miniquad) using [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad)) you can render your 3D content to a texture, then display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use (e.g. [`register_glium_texture`](https://docs.rs/epi/latest/epi/trait.NativeTexture.html#tymethod.register_native_texture)). +There are multiple ways to combine egui with 3D. The simplest way is to use a 3D library and have egui sit on top of the 3D view. See for instance [`bevy_egui`](https://github.com/mvlabat/bevy_egui) or [`three-d`](https://github.com/asny/three-d). -There is an example for showing a native glium texture in an egui window at . +If you want to embed 3D into an egui view there are two options. + +#### `Shape::Callback` +Examples: +* +* + +`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all. + +#### Render-to-texture +You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use. + +Examples: +* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs +* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium): . ## Other diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index 0c1e0b63..5b2f4b40 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -73,3 +73,4 @@ image = { version = "0.24", default-features = false, features = [ ] } poll-promise = "0.1" rfd = "0.8" +three-d = { git = "https://github.com/asny/three-d", rev = "fa475673e284e05b2f4e068769dce3ec5bcabc8d", default-features = false } # 2022-03-22 diff --git a/eframe/examples/custom_3d.rs b/eframe/examples/custom_3d_glow.rs similarity index 96% rename from eframe/examples/custom_3d.rs rename to eframe/examples/custom_3d_glow.rs index 45dedabb..f352367f 100644 --- a/eframe/examples/custom_3d.rs +++ b/eframe/examples/custom_3d_glow.rs @@ -2,7 +2,10 @@ //! //! This is very advanced usage, and you need to be careful. //! -//! If you want an easier way to show 3D graphics with egui, take a look at: +//! If you want an easier way to show 3D graphics with egui, take a look at the `custom_3d_three-d.rs` example. +//! +//! If you are content of having egui sit on top of a 3D background, take a look at: +//! //! * [`bevy_egui`](https://github.com/mvlabat/bevy_egui) //! * [`three-d`](https://github.com/asny/three-d) @@ -49,7 +52,7 @@ impl eframe::App for MyApp { }); egui::ScrollArea::both().show(ui, |ui| { - egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + egui::Frame::canvas(ui.style()).show(ui, |ui| { self.custom_painting(ui); }); ui.label("Drag to rotate!"); diff --git a/eframe/examples/custom_3d_three-d.rs b/eframe/examples/custom_3d_three-d.rs new file mode 100644 index 00000000..606560ce --- /dev/null +++ b/eframe/examples/custom_3d_three-d.rs @@ -0,0 +1,156 @@ +//! This demo shows how to embed 3D rendering using [`three-d`](https://github.com/asny/three-d) in `eframe`. +//! +//! Any 3D library built on top of [`glow`](https://github.com/grovesNL/glow) can be used in `eframe`. +//! +//! Alternatively you can render 3D stuff to a texture and display it using [`egui::Ui::image`]. +//! +//! If you are content of having egui sit on top of a 3D background, take a look at: +//! +//! * [`bevy_egui`](https://github.com/mvlabat/bevy_egui) +//! * [`three-d`](https://github.com/asny/three-d) + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::egui; + +fn main() { + let options = eframe::NativeOptions { + initial_window_size: Some(egui::vec2(550.0, 610.0)), + ..Default::default() + }; + eframe::run_native( + "Custom 3D painting in eframe!", + options, + Box::new(|cc| Box::new(MyApp::new(cc))), + ); +} + +struct MyApp { + angle: f32, +} + +impl MyApp { + fn new(_cc: &eframe::CreationContext<'_>) -> Self { + Self { angle: 0.0 } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + egui::widgets::global_dark_light_mode_buttons(ui); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("The triangle is being painted using "); + ui.hyperlink_to("three-d", "https://github.com/asny/three-d"); + ui.label("."); + }); + + egui::ScrollArea::both().show(ui, |ui| { + egui::Frame::canvas(ui.style()).show(ui, |ui| { + self.custom_painting(ui); + }); + ui.label("Drag to rotate!"); + }); + }); + } +} + +impl MyApp { + fn custom_painting(&mut self, ui: &mut egui::Ui) { + let (rect, response) = + ui.allocate_exact_size(egui::Vec2::splat(512.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 callback = egui::PaintCallback { + rect, + callback: std::sync::Arc::new(move |info, render_ctx| { + if let Some(painter) = render_ctx.downcast_ref::() { + with_three_d_context(painter.gl(), |three_d| { + paint_with_three_d(three_d, info, angle); + }); + } else { + eprintln!("Can't do custom painting because we are not using a glow context"); + } + }), + }; + ui.painter().add(callback); + } +} + +/// We get a [`glow::Context`] from `eframe`, but we want a [`three_d::Context`]. +/// +/// Sadly we can't just create a [`three_d::Context`] in [`MyApp::new`] and pass it +/// to the [`egui::PaintCallback`] because [`three_d::Context`] isn't `Send+Sync`, which +/// [`egui::PaintCallback`] is. +fn with_three_d_context( + gl: &std::rc::Rc, + f: impl FnOnce(&three_d::Context) -> R, +) -> R { + use std::cell::RefCell; + thread_local! { + pub static THREE_D: RefCell> = RefCell::new(None); + } + + THREE_D.with(|three_d| { + let mut three_d = three_d.borrow_mut(); + let three_d = + three_d.get_or_insert_with(|| three_d::Context::from_gl_context(gl.clone()).unwrap()); + f(three_d) + }) +} + +fn paint_with_three_d(three_d: &three_d::Context, info: &egui::PaintCallbackInfo, angle: f32) { + // Based on https://github.com/asny/three-d/blob/master/examples/triangle/src/main.rs + use three_d::*; + + let viewport = Viewport { + x: info.viewport_left_px().round() as _, + y: info.viewport_from_bottom_px().round() as _, + width: info.viewport_width_px().round() as _, + height: info.viewport_height_px().round() as _, + }; + + let camera = Camera::new_perspective( + three_d, + viewport, + vec3(0.0, 0.0, 2.0), + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + degrees(45.0), + 0.1, + 10.0, + ) + .unwrap(); + + // Create a CPU-side mesh consisting of a single colored triangle + let positions = vec![ + vec3(0.5, -0.5, 0.0), // bottom right + vec3(-0.5, -0.5, 0.0), // bottom left + vec3(0.0, 0.5, 0.0), // top + ]; + let colors = vec![ + Color::new(255, 0, 0, 255), // bottom right + Color::new(0, 255, 0, 255), // bottom left + Color::new(0, 0, 255, 255), // top + ]; + let cpu_mesh = CpuMesh { + positions: Positions::F32(positions), + colors: Some(colors), + ..Default::default() + }; + + // Construct a model, with a default color material, thereby transferring the mesh data to the GPU + let mut model = Model::new(three_d, &cpu_mesh).unwrap(); + + // Set the current transformation of the triangle + model.set_transformation(Mat4::from_angle_y(radians(angle))); + + // Render the triangle with the color material which uses the per vertex colors defined at construction + model.render(&camera, &[]).unwrap(); +}