|
2 | 2 |
|
3 | 3 | use std::f32::consts::PI;
|
4 | 4 |
|
| 5 | +use bevy::picking::PickingSystems; |
5 | 6 | use bevy::{
|
6 |
| - asset::RenderAssetUsages, |
| 7 | + asset::{uuid::Uuid, RenderAssetUsages}, |
7 | 8 | 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 | + }, |
9 | 15 | prelude::*,
|
10 | 16 | render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
|
| 17 | + window::{PrimaryWindow, WindowEvent}, |
11 | 18 | };
|
12 | 19 |
|
| 20 | +const CUBE_POINTER_ID: PointerId = PointerId::Custom(Uuid::from_u128(90870987)); |
| 21 | + |
13 | 22 | fn main() {
|
14 | 23 | App::new()
|
15 | 24 | .add_plugins(DefaultPlugins)
|
16 | 25 | .add_systems(Startup, setup)
|
17 | 26 | .add_systems(Update, rotator_system)
|
| 27 | + .add_systems(First, drive_diegetic_pointer.in_set(PickingSystems::Input)) |
18 | 28 | .run();
|
19 | 29 | }
|
20 | 30 |
|
@@ -72,52 +82,166 @@ fn setup(
|
72 | 82 | align_items: AlignItems::Center,
|
73 | 83 | ..default()
|
74 | 84 | },
|
75 |
| - BackgroundColor(GOLD.into()), |
| 85 | + BackgroundColor(GRAY.into()), |
76 | 86 | UiTargetCamera(texture_camera),
|
77 | 87 | ))
|
78 | 88 | .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 | + }); |
87 | 130 | });
|
88 | 131 |
|
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()); |
91 | 133 |
|
92 | 134 | // This material has the texture that has been rendered.
|
93 | 135 | let material_handle = materials.add(StandardMaterial {
|
94 | 136 | base_color_texture: Some(image_handle),
|
95 | 137 | reflectance: 0.02,
|
96 | 138 | unlit: false,
|
97 |
| - |
98 | 139 | ..default()
|
99 | 140 | });
|
100 | 141 |
|
101 | 142 | // Cube with material containing the rendered UI texture.
|
102 | 143 | commands.spawn((
|
103 |
| - Mesh3d(cube_handle), |
| 144 | + Mesh3d(mesh_handle), |
104 | 145 | 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)), |
106 | 147 | Cube,
|
107 | 148 | ));
|
108 | 149 |
|
109 | 150 | // The main pass camera.
|
110 | 151 | commands.spawn((
|
111 | 152 | 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), |
113 | 154 | ));
|
| 155 | + |
| 156 | + commands.spawn(CUBE_POINTER_ID); |
114 | 157 | }
|
115 | 158 |
|
116 |
| -const ROTATION_SPEED: f32 = 0.5; |
| 159 | +const ROTATION_SPEED: f32 = 0.1; |
117 | 160 |
|
118 | 161 | fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Cube>>) {
|
119 | 162 | for mut transform in &mut query {
|
120 | 163 | transform.rotate_x(1.0 * time.delta_secs() * ROTATION_SPEED);
|
121 | 164 | transform.rotate_y(0.7 * time.delta_secs() * ROTATION_SPEED);
|
122 | 165 | }
|
123 | 166 | }
|
| 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