docs/bevy/examples/ui/render_ui_to_texture.rs 7.9 K · 232 lines · raw · history

1 //! Shows how to render UI to a texture. Useful for displaying UI in 3D space.
2
3 use std::f32::consts::PI;
4
5 use bevy::picking::PickingSystems;
6 use bevy::{
7 asset::uuid::Uuid,
8 camera::RenderTarget,
9 color::palettes::css::{BLUE, GRAY, RED},
10 input::ButtonState,
11 picking::{
12 backend::ray::RayMap,
13 pointer::{Location, PointerAction, PointerId, PointerInput},
14 },
15 prelude::*,
16 render::render_resource::TextureFormat,
17 window::{PrimaryWindow, WindowEvent},
18 };
19
20 const CUBE_POINTER_ID: PointerId = PointerId::Custom(Uuid::from_u128(90870987));
21
22 fn main() {
23 App::new()
24 .add_plugins(DefaultPlugins)
25 .add_systems(Startup, setup)
26 .add_systems(Update, rotator_system)
27 .add_systems(First, drive_diegetic_pointer.in_set(PickingSystems::Input))
28 .run();
29 }
30
31 // Marks the cube, to which the UI texture is applied.
32 #[derive(Component)]
33 struct Cube;
34
35 fn setup(
36 mut commands: Commands,
37 mut meshes: ResMut<Assets<Mesh>>,
38 mut materials: ResMut<Assets<StandardMaterial>>,
39 mut images: ResMut<Assets<Image>>,
40 ) {
41 // This is the texture that will be rendered to.
42 let image = Image::new_target_texture(512, 512, TextureFormat::Bgra8UnormSrgb, None);
43
44 let image_handle = images.add(image);
45
46 // Light
47 commands.spawn(DirectionalLight::default());
48
49 let texture_camera = commands
50 .spawn((
51 Camera2d,
52 Camera {
53 // render before the "main pass" camera
54 order: -1,
55 ..default()
56 },
57 RenderTarget::Image(image_handle.clone().into()),
58 ))
59 .id();
60
61 commands
62 .spawn((
63 Node {
64 // Cover the whole image
65 width: percent(100),
66 height: percent(100),
67 flex_direction: FlexDirection::Column,
68 justify_content: JustifyContent::Center,
69 align_items: AlignItems::Center,
70 ..default()
71 },
72 BackgroundColor(GRAY.into()),
73 UiTargetCamera(texture_camera),
74 ))
75 .with_children(|parent| {
76 parent
77 .spawn((
78 Node {
79 position_type: PositionType::Absolute,
80 width: auto(),
81 height: auto(),
82 align_items: AlignItems::Center,
83 padding: UiRect::all(px(20.)),
84 border_radius: BorderRadius::all(px(10.)),
85 ..default()
86 },
87 BackgroundColor(BLUE.into()),
88 ))
89 .observe(
90 |drag: On<PointerDrag>, mut nodes: Query<(&mut Node, &ComputedNode)>| {
91 let (mut node, computed) = nodes.get_mut(drag.entity).unwrap();
92 node.left = px(drag.pointer.position.x - computed.size.x / 2.0);
93 node.top = px(drag.pointer.position.y - 50.0);
94 },
95 )
96 .observe(
97 |over: On<PointerOver>, mut colors: Query<&mut BackgroundColor>| {
98 colors.get_mut(over.entity).unwrap().0 = RED.into();
99 },
100 )
101 .observe(
102 |out: On<PointerOut>, mut colors: Query<&mut BackgroundColor>| {
103 colors.get_mut(out.entity).unwrap().0 = BLUE.into();
104 },
105 )
106 .with_children(|parent| {
107 parent.spawn((
108 Text::new("Drag Me!"),
109 TextFont {
110 font_size: FontSize::Px(40.0),
111 ..default()
112 },
113 TextColor::WHITE,
114 ));
115 });
116 });
117
118 let mesh_handle = meshes.add(Cuboid::default());
119
120 // This material has the texture that has been rendered.
121 let material_handle = materials.add(StandardMaterial {
122 base_color_texture: Some(image_handle),
123 reflectance: 0.02,
124 unlit: false,
125 ..default()
126 });
127
128 // Cube with material containing the rendered UI texture.
129 commands.spawn((
130 Mesh3d(mesh_handle),
131 MeshMaterial3d(material_handle),
132 Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(PI)),
133 Cube,
134 ));
135
136 // The main pass camera.
137 commands.spawn((
138 Camera3d::default(),
139 Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
140 ));
141
142 commands.spawn(CUBE_POINTER_ID);
143 }
144
145 const ROTATION_SPEED: f32 = 0.1;
146
147 fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Cube>>) {
148 for mut transform in &mut query {
149 transform.rotate_x(1.0 * time.delta_secs() * ROTATION_SPEED);
150 transform.rotate_y(0.7 * time.delta_secs() * ROTATION_SPEED);
151 }
152 }
153
154 /// Because bevy has no way to know how to map a mouse input to the UI texture, we need to write a
155 /// system that tells it there is a pointer on the UI texture. We cast a ray into the scene and find
156 /// the UV (2D texture) coordinates of the raycast hit. This UV coordinate is effectively the same
157 /// as a pointer coordinate on a 2D UI rect.
158 fn drive_diegetic_pointer(
159 mut cursor_last: Local<Vec2>,
160 mut raycast: MeshRayCast,
161 rays: Res<RayMap>,
162 cubes: Query<&Mesh3d, With<Cube>>,
163 ui_camera: Query<&RenderTarget, With<Camera2d>>,
164 primary_window: Query<Entity, With<PrimaryWindow>>,
165 windows: Query<(Entity, &Window)>,
166 images: Res<Assets<Image>>,
167 manual_texture_views: Res<ManualTextureViews>,
168 mut window_events: MessageReader<WindowEvent>,
169 mut pointer_inputs: MessageWriter<PointerInput>,
170 ) -> Result {
171 // Get the size of the texture, so we can convert from dimensionless UV coordinates that span
172 // from 0 to 1, to pixel coordinates.
173 let target = ui_camera
174 .single()?
175 .normalize(primary_window.single().ok())
176 .unwrap();
177 let target_info = target
178 .get_render_target_info(windows, &images, &manual_texture_views)
179 .unwrap();
180 let size = target_info.physical_size.as_vec2();
181
182 // Find raycast hits and update the virtual pointer.
183 let raycast_settings = MeshRayCastSettings {
184 visibility: RayCastVisibility::VisibleInView,
185 filter: &|entity| cubes.contains(entity),
186 early_exit_test: &|_| false,
187 };
188 for (_id, ray) in rays.iter() {
189 for (_cube, hit) in raycast.cast_ray(*ray, &raycast_settings) {
190 let position = size * hit.uv.unwrap();
191 if position != *cursor_last {
192 pointer_inputs.write(PointerInput::new(
193 CUBE_POINTER_ID,
194 Location {
195 target: target.clone(),
196 position,
197 },
198 PointerAction::Move {
199 delta: position - *cursor_last,
200 },
201 ));
202 *cursor_last = position;
203 }
204 }
205 }
206
207 // Pipe pointer button presses to the virtual pointer on the UI texture.
208 for window_event in window_events.read() {
209 if let WindowEvent::MouseButtonInput(input) = window_event {
210 let button = match input.button {
211 MouseButton::Left => PointerButton::Primary,
212 MouseButton::Right => PointerButton::Secondary,
213 MouseButton::Middle => PointerButton::Middle,
214 _ => continue,
215 };
216 let action = match input.state {
217 ButtonState::Pressed => PointerAction::Press(button),
218 ButtonState::Released => PointerAction::Release(button),
219 };
220 pointer_inputs.write(PointerInput::new(
221 CUBE_POINTER_ID,
222 Location {
223 target: target.clone(),
224 position: *cursor_last,
225 },
226 action,
227 ));
228 }
229 }
230
231 Ok(())
232 }