Shows how to render UI to a texture. Useful for displaying UI in 3D space.
use std::f32::consts::PI;
use bevy::picking::PickingSystems;
use bevy::{
asset::{uuid::Uuid, RenderAssetUsages},
camera::RenderTarget,
color::palettes::css::{BLUE, GRAY, RED},
input::ButtonState,
picking::{
backend::ray::RayMap,
pointer::{Location, PointerAction, PointerId, PointerInput},
},
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
window::{PrimaryWindow, WindowEvent},
};
const CUBE_POINTER_ID: PointerId = PointerId::Custom(Uuid::from_u128(90870987));
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, rotator_system)
.add_systems(First, drive_diegetic_pointer.in_set(PickingSystems::Input))
.run();
}
// Marks the cube, to which the UI texture is applied.
#[derive(Component)]
struct Cube;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
let size = Extent3d {
width: 512,
height: 512,
..default()
};
// This is the texture that will be rendered to.
let mut image = Image::new_fill(
size,
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::default(),
);
// You need to set these texture usage flags in order to use the image as a render target
image.texture_descriptor.usage =
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
let image_handle = images.add(image);
// Light
commands.spawn(DirectionalLight::default());
let texture_camera = commands
.spawn((
Camera2d,
Camera {
// render before the "main pass" camera
order: -1,
target: RenderTarget::Image(image_handle.clone().into()),
..default()
},
))
.id();
commands
.spawn((
Node {
// Cover the whole image
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(GRAY.into()),
UiTargetCamera(texture_camera),
))
.with_children(|parent| {
parent
.spawn((
Node {
position_type: PositionType::Absolute,
width: Val::Auto,
height: Val::Auto,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.)),
..default()
},
BorderRadius::all(Val::Px(10.)),
BackgroundColor(BLUE.into()),
))
.observe(
|drag: On<Pointer<Drag>>, mut nodes: Query<(&mut Node, &ComputedNode)>| {
let (mut node, computed) = nodes.get_mut(drag.entity).unwrap();
node.left =
Val::Px(drag.pointer_location.position.x - computed.size.x / 2.0);
node.top = Val::Px(drag.pointer_location.position.y - 50.0);
},
)
.observe(
|over: On<Pointer<Over>>, mut colors: Query<&mut BackgroundColor>| {
colors.get_mut(over.entity).unwrap().0 = RED.into();
},
)
.observe(
|out: On<Pointer<Out>>, mut colors: Query<&mut BackgroundColor>| {
colors.get_mut(out.entity).unwrap().0 = BLUE.into();
},
)
.with_children(|parent| {
parent.spawn((
Text::new("Drag Me!"),
TextFont {
font_size: 40.0,
..default()
},
TextColor::WHITE,
));
});
});
let mesh_handle = meshes.add(Cuboid::default());
// This material has the texture that has been rendered.
let material_handle = materials.add(StandardMaterial {
base_color_texture: Some(image_handle),
reflectance: 0.02,
unlit: false,
..default()
});
// Cube with material containing the rendered UI texture.
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(material_handle),
Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(PI)),
Cube,
));
// The main pass camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn(CUBE_POINTER_ID);
}
const ROTATION_SPEED: f32 = 0.1;
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Cube>>) {
for mut transform in &mut query {
transform.rotate_x(1.0 * time.delta_secs() * ROTATION_SPEED);
transform.rotate_y(0.7 * time.delta_secs() * ROTATION_SPEED);
}
}
/// Because bevy has no way to know how to map a mouse input to the UI texture, we need to write a
/// system that tells it there is a pointer on the UI texture. We cast a ray into the scene and find
/// the UV (2D texture) coordinates of the raycast hit. This UV coordinate is effectively the same
/// as a pointer coordinate on a 2D UI rect.
fn drive_diegetic_pointer(
mut cursor_last: Local<Vec2>,
mut raycast: MeshRayCast,
rays: Res<RayMap>,
cubes: Query<&Mesh3d, With<Cube>>,
ui_camera: Query<&Camera, With<Camera2d>>,
primary_window: Query<Entity, With<PrimaryWindow>>,
windows: Query<(Entity, &Window)>,
images: Res<Assets<Image>>,
manual_texture_views: Res<ManualTextureViews>,
mut window_events: MessageReader<WindowEvent>,
mut pointer_inputs: MessageWriter<PointerInput>,
) -> Result {
// Get the size of the texture, so we can convert from dimensionless UV coordinates that span
// from 0 to 1, to pixel coordinates.
let target = ui_camera
.single()?
.target
.normalize(primary_window.single().ok())
.unwrap();
let target_info = target
.get_render_target_info(windows, &images, &manual_texture_views)
.unwrap();
let size = target_info.physical_size.as_vec2();
// Find raycast hits and update the virtual pointer.
let raycast_settings = MeshRayCastSettings {
visibility: RayCastVisibility::VisibleInView,
filter: &|entity| cubes.contains(entity),
early_exit_test: &|_| false,
};
for (_id, ray) in rays.iter() {
for (_cube, hit) in raycast.cast_ray(*ray, &raycast_settings) {
let position = size * hit.uv.unwrap();
if position != *cursor_last {
pointer_inputs.write(PointerInput::new(
CUBE_POINTER_ID,
Location {
target: target.clone(),
position,
},
PointerAction::Move {
delta: position - *cursor_last,
},
));
*cursor_last = position;
}
}
}
// Pipe pointer button presses to the virtual pointer on the UI texture.
for window_event in window_events.read() {
if let WindowEvent::MouseButtonInput(input) = window_event {
let button = match input.button {
MouseButton::Left => PointerButton::Primary,
MouseButton::Right => PointerButton::Secondary,
MouseButton::Middle => PointerButton::Middle,
_ => continue,
};
let action = match input.state {
ButtonState::Pressed => PointerAction::Press(button),
ButtonState::Released => PointerAction::Release(button),
};
pointer_inputs.write(PointerInput::new(
CUBE_POINTER_ID,
Location {
target: target.clone(),
position: *cursor_last,
},
action,
));
}
}
Ok(())
}