Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Worldspace UI #23

Merged
merged 12 commits into from
Sep 19, 2023
Merged
342 changes: 342 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"skills/cube",
"skills/worldspace-ui",
"skills/ik",
"skills/manipulation-flatscreen",
"skills/openxr-6dof",
Expand All @@ -22,6 +23,12 @@ bevy_mod_picking = "0.15.0"
color-eyre = "0.6"
eyre = "0.6"
tracing = "0.1"
bevy_egui = "0.21"
egui-wgpu = "0.22"
egui = "0.22"
bevy-inspector-egui = "0.19"
wgpu = "0.16"
bevy_flycam = { git = "https://github.com/sburris0/bevy_flycam", branch = "bevy_0.11" }

# Enable a small amount of optimization in debug mode
[profile.dev]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Feel free to add to the following lists:
- ✅ [Single textured 3d cube](skills/cube)
- 📋 Flycam camera controller
- 📋 First person camera controller (for desktop users)
- 📋 In game UI ( egui, bevyui, whatever )
- ✅ [World-space UI](skills/worldspace-ui) (with egui)
- 📋 Entity inspector (`bevy_inspector_egui`)
- 📋 Custom vertex and fragment shader in WGSL + bevy
- ✅ [Render a GLTF/VRM model (standard shader)](skills/ik)
Expand Down
16 changes: 16 additions & 0 deletions skills/worldspace-ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "worldspace-ui"
version.workspace = true
license.workspace = true
repository.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
bevy.workspace = true
bevy_egui.workspace = true
bevy_flycam.workspace = true
color-eyre.workspace = true
egui-wgpu.workspace = true
egui.workspace = true
wgpu.workspace = true
11 changes: 11 additions & 0 deletions skills/worldspace-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `worldspace-ui`

A cube that renders an [egui](https://www.egui.rs/#demo) application to a texture,
and applies it to a cube.

To run the code:
```bash
cargo run -p worldspace-ui
```

![Screenshot](screenshot.png)
3 changes: 3 additions & 0 deletions skills/worldspace-ui/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions skills/worldspace-ui/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
mod render_node;
mod render_systems;

use bevy::prelude::*;
use bevy_egui::egui;
use bevy_egui::EguiContexts;
use color_eyre::Result;

fn main() -> Result<()> {
color_eyre::install()?;
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins(bevy_flycam::PlayerPlugin)
.add_plugins(bevy_egui::EguiPlugin)
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.,
})
.add_systems(Startup, setup)
.add_systems(Update, screenspace_ui);

let Ok(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) else {
panic!("The render plugin should have added this subapp");
};
render_app.add_systems(ExtractSchedule, crate::render_systems::add_render_node);

app.run();
Ok(())
}

#[derive(Component, Clone)]
pub struct EguiContext {
output_texture: Handle<Image>,
ctx: egui::Context,
}

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
let egui_thing = {
let size = wgpu::Extent3d {
width: 256,
height: 256,
depth_or_array_layers: 1,
};
let mut output_texture = Image {
data: vec![255; (size.width * size.height * 4) as usize],
..default()
};
output_texture.texture_descriptor.usage |=
wgpu::TextureUsages::RENDER_ATTACHMENT;
output_texture.texture_descriptor.size = size;
let output_texture = images.add(output_texture);

EguiContext {
output_texture,
ctx: egui::Context::default(),
}
};

commands.spawn((
PbrBundle {
mesh: meshes.add(shape::Cube::default().into()),
material: materials.add(StandardMaterial {
base_color: Color::WHITE,
base_color_texture: Some(Handle::clone(&egui_thing.output_texture)),
// Remove this if you want it to use the world's lighting.
unlit: true,
..default()
}),
..default()
},
egui_thing,
));
}

fn screenspace_ui(mut contexts: EguiContexts) {
egui::Window::new("Screenspace Window").show(contexts.ctx_mut(), |ui| {
ui.label("I am rendering to the screen!");
});
}
88 changes: 88 additions & 0 deletions skills/worldspace-ui/src/render_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use bevy::{prelude::*, render::render_asset::RenderAssets};
use std::sync::Mutex;

use crate::EguiContext;

pub struct EguiNode {
pub output_image: Handle<Image>,
pub egui_ctx: EguiContext,
pub renderer: Mutex<egui_wgpu::Renderer>,
}

impl bevy::render::render_graph::Node for EguiNode {
// TODO: When should I use this instead of an Extract render system?
fn update(&mut self, _world: &mut World) {}

fn run(
&self,
_graph: &mut bevy::render::render_graph::RenderGraphContext,
render_context: &mut bevy::render::renderer::RenderContext,
world: &World,
) -> Result<(), bevy::render::render_graph::NodeRunError> {
let device = render_context.render_device().clone();
let device = device.wgpu_device();
let queue = world
.get_resource::<bevy::render::renderer::RenderQueue>()
.unwrap();
let encoder = render_context.command_encoder();
let gpu_images = world.get_resource::<RenderAssets<Image>>().unwrap();
let output_gpu_image = gpu_images
.get(&self.output_image)
.expect("Should have been a `GpuImage` that corresponds to the `Image`");
let screen_descriptor = egui_wgpu::renderer::ScreenDescriptor {
pixels_per_point: 1.0,
size_in_pixels: [
output_gpu_image.texture.size().width,
output_gpu_image.texture.size().height,
],
};

let mut renderer = self.renderer.lock().unwrap();

// TODO: Eventually I'll move this to a separate user defined system.
let egui_output = self.egui_ctx.ctx.run(egui::RawInput::default(), |ctx| {
egui::Window::new("Worldspace Window")
.show(ctx, |ui| ui.label("I am rendering to a texture on a cube"));
});
// TODO: Handle textures to delete
for (tid, delta) in egui_output.textures_delta.set.iter() {
renderer.update_texture(device, queue, *tid, delta);
}
let clipped_primitives = self.egui_ctx.ctx.tessellate(egui_output.shapes);

renderer.update_buffers(
device,
queue,
encoder,
&clipped_primitives,
&screen_descriptor,
);

let mut egui_render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Egui Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &output_gpu_image.texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: true,
},
})],
depth_stencil_attachment: None,
});

renderer.render(
&mut egui_render_pass,
&clipped_primitives,
&screen_descriptor,
);

Ok(())
}
}
37 changes: 37 additions & 0 deletions skills/worldspace-ui/src/render_systems.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use bevy::render::Extract;
use bevy::{prelude::*, render::render_graph::RenderGraph};

use crate::render_node::EguiNode;
use crate::EguiContext;

pub fn add_render_node(
q: Extract<Query<(Entity, &EguiContext), Added<EguiContext>>>,
// q: Extract<Query<&EguiContext>>,
textures: Extract<Res<Assets<Image>>>,
mut render_graph: ResMut<RenderGraph>,
device: Res<bevy::render::renderer::RenderDevice>,
) {
for (entity, egui_ctx) in q.iter() {
info!("adding render node for {entity:?}");
let output_texture_format = textures
.get(&egui_ctx.output_texture)
.expect("Should have found the matching `Image`")
.texture_descriptor
.format;
let renderer = egui_wgpu::Renderer::new(
device.wgpu_device(),
output_texture_format,
None,
1,
);
let new_node = EguiNode {
output_image: Handle::clone(&egui_ctx.output_texture),
renderer: renderer.into(),
egui_ctx: egui_ctx.clone(),
};
let node_label = "egui-texture";
render_graph.add_node(node_label, new_node);
render_graph
.add_node_edge(bevy::render::main_graph::node::CAMERA_DRIVER, node_label);
}
}