Skip to main content

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,
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
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    // 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
145const ROTATION_SPEED: f32 = 0.1;
146
147fn 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.
158fn 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}