Skip to content

Commit b094540

Browse files
aevyrieIceSentryalice-i-cecile
authored
Diegetic UI example (#19199)
https://github.com/user-attachments/assets/e7b428b9-4705-4d0a-8322-c7a80994b1a4 This demonstrates general-purpose world-space picking. This is not limited to bevy_ui, it works with any render target and any picking backend, e.g. this could be a viewport/portal in 3d space, it could be a custom UI system, bevy_egui, sprites, etc, etc. Because this uses UV coordinates, it also works with anything that can be mapped to a surface, so you can display UIs on curved surfaces, etc. https://github.com/user-attachments/assets/2926a5a1-16c9-4179-85fa-83a69fd55a96 ### Discussion - This illustrates some of the improvements I think we need to make to picking - the picking ID should be removed in favor of a pointer kind - you might have multiple mouse pointers on multiple surfaces, with different unique IDs. It's probably worth revisiting if we can use Entity as the ID instead of a custom one. When I first wrote this, I ran into some unexpected issues that caused with pointers being despawned and getting a new ID. We will just need to be careful of edge cases there. --------- Co-authored-by: IceSentry <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent ae143d4 commit b094540

File tree

1 file changed

+142
-18
lines changed

1 file changed

+142
-18
lines changed

examples/ui/render_ui_to_texture.rs

Lines changed: 142 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,29 @@
22
33
use std::f32::consts::PI;
44

5+
use bevy::picking::PickingSystems;
56
use bevy::{
6-
asset::RenderAssetUsages,
7+
asset::{uuid::Uuid, RenderAssetUsages},
78
camera::RenderTarget,
8-
color::palettes::css::GOLD,
9+
color::palettes::css::{BLUE, GRAY, RED},
10+
input::ButtonState,
11+
picking::{
12+
backend::ray::RayMap,
13+
pointer::{Location, PointerAction, PointerId, PointerInput},
14+
},
915
prelude::*,
1016
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
17+
window::{PrimaryWindow, WindowEvent},
1118
};
1219

20+
const CUBE_POINTER_ID: PointerId = PointerId::Custom(Uuid::from_u128(90870987));
21+
1322
fn main() {
1423
App::new()
1524
.add_plugins(DefaultPlugins)
1625
.add_systems(Startup, setup)
1726
.add_systems(Update, rotator_system)
27+
.add_systems(First, drive_diegetic_pointer.in_set(PickingSystems::Input))
1828
.run();
1929
}
2030

@@ -72,52 +82,166 @@ fn setup(
7282
align_items: AlignItems::Center,
7383
..default()
7484
},
75-
BackgroundColor(GOLD.into()),
85+
BackgroundColor(GRAY.into()),
7686
UiTargetCamera(texture_camera),
7787
))
7888
.with_children(|parent| {
79-
parent.spawn((
80-
Text::new("This is a cube"),
81-
TextFont {
82-
font_size: 40.0,
83-
..default()
84-
},
85-
TextColor::BLACK,
86-
));
89+
parent
90+
.spawn((
91+
Node {
92+
position_type: PositionType::Absolute,
93+
width: Val::Auto,
94+
height: Val::Auto,
95+
align_items: AlignItems::Center,
96+
padding: UiRect::all(Val::Px(20.)),
97+
..default()
98+
},
99+
BorderRadius::all(Val::Px(10.)),
100+
BackgroundColor(BLUE.into()),
101+
))
102+
.observe(
103+
|pointer: On<Pointer<Drag>>, mut nodes: Query<(&mut Node, &ComputedNode)>| {
104+
let (mut node, computed) = nodes.get_mut(pointer.target()).unwrap();
105+
node.left =
106+
Val::Px(pointer.pointer_location.position.x - computed.size.x / 2.0);
107+
node.top = Val::Px(pointer.pointer_location.position.y - 50.0);
108+
},
109+
)
110+
.observe(
111+
|pointer: On<Pointer<Over>>, mut colors: Query<&mut BackgroundColor>| {
112+
colors.get_mut(pointer.target()).unwrap().0 = RED.into();
113+
},
114+
)
115+
.observe(
116+
|pointer: On<Pointer<Out>>, mut colors: Query<&mut BackgroundColor>| {
117+
colors.get_mut(pointer.target()).unwrap().0 = BLUE.into();
118+
},
119+
)
120+
.with_children(|parent| {
121+
parent.spawn((
122+
Text::new("Drag Me!"),
123+
TextFont {
124+
font_size: 40.0,
125+
..default()
126+
},
127+
TextColor::WHITE,
128+
));
129+
});
87130
});
88131

89-
let cube_size = 4.0;
90-
let cube_handle = meshes.add(Cuboid::new(cube_size, cube_size, cube_size));
132+
let mesh_handle = meshes.add(Cuboid::default());
91133

92134
// This material has the texture that has been rendered.
93135
let material_handle = materials.add(StandardMaterial {
94136
base_color_texture: Some(image_handle),
95137
reflectance: 0.02,
96138
unlit: false,
97-
98139
..default()
99140
});
100141

101142
// Cube with material containing the rendered UI texture.
102143
commands.spawn((
103-
Mesh3d(cube_handle),
144+
Mesh3d(mesh_handle),
104145
MeshMaterial3d(material_handle),
105-
Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(-PI / 5.0)),
146+
Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(PI)),
106147
Cube,
107148
));
108149

109150
// The main pass camera.
110151
commands.spawn((
111152
Camera3d::default(),
112-
Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
153+
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
113154
));
155+
156+
commands.spawn(CUBE_POINTER_ID);
114157
}
115158

116-
const ROTATION_SPEED: f32 = 0.5;
159+
const ROTATION_SPEED: f32 = 0.1;
117160

118161
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Cube>>) {
119162
for mut transform in &mut query {
120163
transform.rotate_x(1.0 * time.delta_secs() * ROTATION_SPEED);
121164
transform.rotate_y(0.7 * time.delta_secs() * ROTATION_SPEED);
122165
}
123166
}
167+
168+
/// Because bevy has no way to know how to map a mouse input to the UI texture, we need to write a
169+
/// system that tells it there is a pointer on the UI texture. We cast a ray into the scene and find
170+
/// the UV (2D texture) coordinates of the raycast hit. This UV coordinate is effectively the same
171+
/// as a pointer coordinate on a 2D UI rect.
172+
fn drive_diegetic_pointer(
173+
mut cursor_last: Local<Vec2>,
174+
mut raycast: MeshRayCast,
175+
rays: Res<RayMap>,
176+
cubes: Query<&Mesh3d, With<Cube>>,
177+
ui_camera: Query<&Camera, With<Camera2d>>,
178+
primary_window: Query<Entity, With<PrimaryWindow>>,
179+
windows: Query<(Entity, &Window)>,
180+
images: Res<Assets<Image>>,
181+
manual_texture_views: Res<ManualTextureViews>,
182+
mut window_events: EventReader<WindowEvent>,
183+
mut pointer_input: EventWriter<PointerInput>,
184+
) -> Result {
185+
// Get the size of the texture, so we can convert from dimensionless UV coordinates that span
186+
// from 0 to 1, to pixel coordinates.
187+
let target = ui_camera
188+
.single()?
189+
.target
190+
.normalize(primary_window.single().ok())
191+
.unwrap();
192+
let target_info = target
193+
.get_render_target_info(windows, &images, &manual_texture_views)
194+
.unwrap();
195+
let size = target_info.physical_size.as_vec2();
196+
197+
// Find raycast hits and update the virtual pointer.
198+
let raycast_settings = MeshRayCastSettings {
199+
visibility: RayCastVisibility::VisibleInView,
200+
filter: &|entity| cubes.contains(entity),
201+
early_exit_test: &|_| false,
202+
};
203+
for (_id, ray) in rays.iter() {
204+
for (_cube, hit) in raycast.cast_ray(*ray, &raycast_settings) {
205+
let position = size * hit.uv.unwrap();
206+
if position != *cursor_last {
207+
pointer_input.write(PointerInput::new(
208+
CUBE_POINTER_ID,
209+
Location {
210+
target: target.clone(),
211+
position,
212+
},
213+
PointerAction::Move {
214+
delta: position - *cursor_last,
215+
},
216+
));
217+
*cursor_last = position;
218+
}
219+
}
220+
}
221+
222+
// Pipe pointer button presses to the virtual pointer on the UI texture.
223+
for window_event in window_events.read() {
224+
if let WindowEvent::MouseButtonInput(input) = window_event {
225+
let button = match input.button {
226+
MouseButton::Left => PointerButton::Primary,
227+
MouseButton::Right => PointerButton::Secondary,
228+
MouseButton::Middle => PointerButton::Middle,
229+
_ => continue,
230+
};
231+
let action = match input.state {
232+
ButtonState::Pressed => PointerAction::Press(button),
233+
ButtonState::Released => PointerAction::Release(button),
234+
};
235+
pointer_input.write(PointerInput::new(
236+
CUBE_POINTER_ID,
237+
Location {
238+
target: target.clone(),
239+
position: *cursor_last,
240+
},
241+
action,
242+
));
243+
}
244+
}
245+
246+
Ok(())
247+
}

0 commit comments

Comments
 (0)