render_ui_to_texture/
render_ui_to_texture.rs

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