use bevy::camera::{ImageRenderTarget, NormalizedRenderTarget, RenderTarget as BevyRenderTarget};
use bevy::mesh::{Indices, VertexAttributeValues};
use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayMeshHit};
use bevy::picking::pointer::{Location, PointerAction, PointerButton, PointerId, PointerInput};
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::render::render_resource::TextureFormat;
pub use bevy::mesh::UvChannel;
const MAX_DIM: u32 = 4096;
const SURFACE_POINTER_UUID: uuid::Uuid =
uuid::Uuid::from_u128(0xB5_2E_5F_AC_E0_00_00_00_00_00_00_00_00_00_01);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderMode {
Live,
Snapshot,
}
#[derive(Clone, Copy, Debug)]
pub struct SurfaceSpec {
pub size: UVec2,
pub clear_color: Color,
pub mode: RenderMode,
}
impl Default for SurfaceSpec {
fn default() -> Self {
Self {
size: UVec2::new(512, 512),
clear_color: Color::BLACK,
mode: RenderMode::Live,
}
}
}
struct Entry {
handle: Handle<Image>,
size: UVec2,
clear_color: Color,
mode: RenderMode,
camera: Option<Entity>,
dirty: bool,
}
#[derive(Resource, Default)]
pub struct Surfaces {
entries: HashMap<String, Entry>,
}
impl Surfaces {
pub fn create(
&mut self,
images: &mut Assets<Image>,
name: impl Into<String>,
spec: SurfaceSpec,
) -> Handle<Image> {
let size = spec.size.max(UVec2::ONE).min(UVec2::splat(MAX_DIM));
let image = Image::new_target_texture(size.x, size.y, TextureFormat::Rgba8UnormSrgb, None);
let handle = images.add(image);
self.entries.insert(
name.into(),
Entry {
handle: handle.clone(),
size,
clear_color: spec.clear_color,
mode: spec.mode,
camera: None,
dirty: true,
},
);
handle
}
pub fn get(&self, name: &str) -> Option<Handle<Image>> {
self.entries.get(name).map(|e| e.handle.clone())
}
pub fn invalidate(&mut self, name: &str) {
if let Some(e) = self.entries.get_mut(name) {
e.dirty = true;
}
}
pub fn set_mode(&mut self, name: &str, mode: RenderMode) {
if let Some(e) = self.entries.get_mut(name) {
if e.mode != mode {
e.dirty = true;
}
e.mode = mode;
}
}
pub fn remove(&mut self, name: &str) {
self.entries.remove(name);
}
}
#[derive(Component, Clone, Debug)]
pub struct RSurface(pub String);
#[derive(Component, Clone, Debug)]
pub struct SurfaceCamera(pub String);
#[derive(Component, Clone, Debug)]
pub struct SurfacePointer {
pub surface: String,
pub uv_channel: UvChannel,
}
impl SurfacePointer {
pub fn new(surface: impl Into<String>) -> Self {
Self {
surface: surface.into(),
uv_channel: UvChannel::Uv0,
}
}
pub fn with_uv_channel(mut self, channel: UvChannel) -> Self {
self.uv_channel = channel;
self
}
}
#[derive(Resource)]
pub struct SurfaceVirtualPointer {
pub id: PointerId,
last_pos: Vec2,
over_target: Option<Handle<Image>>,
pressed: [bool; FORWARDED_BUTTONS.len()],
}
const FORWARDED_BUTTONS: [(MouseButton, PointerButton); 3] = [
(MouseButton::Left, PointerButton::Primary),
(MouseButton::Right, PointerButton::Secondary),
(MouseButton::Middle, PointerButton::Middle),
];
fn button_index(button: PointerButton) -> usize {
match button {
PointerButton::Primary => 0,
PointerButton::Secondary => 1,
PointerButton::Middle => 2,
}
}
pub fn init_surface_pointer(mut commands: Commands) {
let id = PointerId::Custom(SURFACE_POINTER_UUID);
commands.spawn(id);
commands.insert_resource(SurfaceVirtualPointer {
id,
last_pos: Vec2::ZERO,
over_target: None,
pressed: [false; FORWARDED_BUTTONS.len()],
});
}
pub fn bind_surfaces(
mut commands: Commands,
mut surfaces: ResMut<Surfaces>,
mut roots: Query<(Entity, &RSurface, Option<&UiTargetCamera>, &mut Visibility)>,
) {
for (name, entry) in surfaces.entries.iter_mut() {
if entry.camera.is_some() {
continue;
}
let camera = commands
.spawn((
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(entry.clear_color),
order: -1,
..default()
},
BevyRenderTarget::Image(ImageRenderTarget {
handle: entry.handle.clone(),
scale_factor: 1.0,
}),
SurfaceCamera(name.clone()),
))
.id();
entry.camera = Some(camera);
entry.dirty = true;
}
for (entity, surface, target_cam, mut visibility) in &mut roots {
match surfaces.entries.get(&surface.0).and_then(|e| e.camera) {
Some(camera) => {
if target_cam.map(|t| t.0) != Some(camera) {
commands.entity(entity).insert(UiTargetCamera(camera));
}
if *visibility != Visibility::Inherited {
*visibility = Visibility::Inherited;
}
}
None => {
if target_cam.is_some() {
commands.entity(entity).remove::<UiTargetCamera>();
}
if *visibility != Visibility::Hidden {
*visibility = Visibility::Hidden;
}
}
}
}
}
pub fn drive_surfaces(
mut commands: Commands,
mut surfaces: ResMut<Surfaces>,
mut cameras: Query<(Entity, &SurfaceCamera, &mut Camera)>,
) {
for (entity, cam, mut camera) in &mut cameras {
let Some(entry) = surfaces.entries.get_mut(&cam.0) else {
commands.entity(entity).despawn();
continue;
};
if entry.camera != Some(entity) {
commands.entity(entity).despawn();
continue;
}
match entry.mode {
RenderMode::Live => {
if !camera.is_active {
camera.is_active = true;
}
}
RenderMode::Snapshot => {
let active = entry.dirty;
if camera.is_active != active {
camera.is_active = active;
}
entry.dirty = false;
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn drive_surface_pointer(
surfaces: Res<Surfaces>,
mut state: ResMut<SurfaceVirtualPointer>,
windows: Query<&Window>,
cameras: Query<(&Camera, &BevyRenderTarget, &GlobalTransform)>,
pointer_meshes: Query<&SurfacePointer>,
mesh3ds: Query<&Mesh3d>,
meshes: Res<Assets<Mesh>>,
buttons: Res<ButtonInput<MouseButton>>,
mut ray_cast: MeshRayCast,
mut input: MessageWriter<PointerInput>,
) {
let pointer_id = state.id;
let hit = cursor_ray(&windows, &cameras).and_then(|ray| {
let filter = |entity: Entity| pointer_meshes.contains(entity);
let settings = MeshRayCastSettings::default().with_filter(&filter);
ray_cast
.cast_ray(ray, &settings)
.first()
.map(|(entity, hit)| (*entity, hit.clone()))
});
if let Some((entity, hit)) = hit
&& let Ok(pointer) = pointer_meshes.get(entity)
&& let Some(handle) = surfaces.get(&pointer.surface)
&& let Some(size) = surfaces.entries.get(&pointer.surface).map(|e| e.size)
&& let Some(uv) = hit_uv(&pointer.uv_channel, &hit, entity, &mesh3ds, &meshes)
{
let position = Vec2::new(uv.x * size.x as f32, uv.y * size.y as f32);
let location = image_location(&handle, position);
let delta = position - state.last_pos;
input.write(PointerInput::new(
pointer_id,
location.clone(),
PointerAction::Move { delta },
));
state.last_pos = position;
state.over_target = Some(handle);
for (mb, pb) in FORWARDED_BUTTONS {
if buttons.just_pressed(mb) {
input.write(PointerInput::new(
pointer_id,
location.clone(),
PointerAction::Press(pb),
));
state.pressed[button_index(pb)] = true;
}
if buttons.just_released(mb) && state.pressed[button_index(pb)] {
input.write(PointerInput::new(
pointer_id,
location.clone(),
PointerAction::Release(pb),
));
state.pressed[button_index(pb)] = false;
}
}
return;
}
if let Some(handle) = state.over_target.clone() {
let location = image_location(&handle, Vec2::splat(-1.0));
for (_, pb) in FORWARDED_BUTTONS {
if state.pressed[button_index(pb)] {
input.write(PointerInput::new(
pointer_id,
location.clone(),
PointerAction::Release(pb),
));
state.pressed[button_index(pb)] = false;
}
}
input.write(PointerInput::new(
pointer_id,
location,
PointerAction::Move { delta: Vec2::ZERO },
));
state.over_target = None;
}
}
fn hit_uv(
channel: &UvChannel,
hit: &RayMeshHit,
entity: Entity,
mesh3ds: &Query<&Mesh3d>,
meshes: &Assets<Mesh>,
) -> Option<Vec2> {
match channel {
UvChannel::Uv0 => hit.uv,
UvChannel::Uv1 => {
let mesh = meshes.get(&mesh3ds.get(entity).ok()?.0)?;
let VertexAttributeValues::Float32x2(uvs) = mesh.attribute(Mesh::ATTRIBUTE_UV_1)?
else {
return None;
};
let base = hit.triangle_index? * 3;
let vertex = |k: usize| -> Option<usize> {
Some(match mesh.indices() {
Some(Indices::U16(v)) => *v.get(base + k)? as usize,
Some(Indices::U32(v)) => *v.get(base + k)? as usize,
None => base + k,
})
};
let uv = |k: usize| -> Option<Vec2> { uvs.get(vertex(k)?).map(|&p| Vec2::from(p)) };
let bc = hit.barycentric_coords;
Some(bc.x * uv(0)? + bc.y * uv(1)? + bc.z * uv(2)?)
}
}
}
fn image_location(handle: &Handle<Image>, position: Vec2) -> Location {
Location {
target: NormalizedRenderTarget::Image(ImageRenderTarget {
handle: handle.clone(),
scale_factor: 1.0,
}),
position,
}
}
fn cursor_ray(
windows: &Query<&Window>,
cameras: &Query<(&Camera, &BevyRenderTarget, &GlobalTransform)>,
) -> Option<Ray3d> {
let cursor = windows.iter().find_map(|w| w.cursor_position())?;
let (camera, _, transform) = cameras
.iter()
.find(|(c, target, _)| c.is_active && matches!(target, BevyRenderTarget::Window(_)))?;
camera.viewport_to_world(transform, cursor).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, AssetPlugin::default()));
app.init_asset::<Image>();
app.init_resource::<Surfaces>();
app
}
#[test]
fn create_get_remove() {
let mut app = test_app();
let handle = app
.world_mut()
.resource_scope(|world, mut surfaces: Mut<Surfaces>| {
let mut images = world.resource_mut::<Assets<Image>>();
surfaces.create(&mut images, "monitor", SurfaceSpec::default())
});
assert_eq!(
app.world().resource::<Surfaces>().get("monitor"),
Some(handle)
);
assert_eq!(app.world().resource::<Surfaces>().get("nope"), None);
app.world_mut().resource_mut::<Surfaces>().remove("monitor");
assert_eq!(app.world().resource::<Surfaces>().get("monitor"), None);
}
#[test]
fn set_mode_and_invalidate_mark_dirty() {
let mut app = test_app();
app.world_mut()
.resource_scope(|world, mut surfaces: Mut<Surfaces>| {
let mut images = world.resource_mut::<Assets<Image>>();
surfaces.create(
&mut images,
"monitor",
SurfaceSpec {
mode: RenderMode::Live,
..default()
},
);
});
let mut surfaces = app.world_mut().resource_mut::<Surfaces>();
surfaces.entries.get_mut("monitor").unwrap().dirty = false;
surfaces.set_mode("monitor", RenderMode::Live); assert!(!surfaces.entries["monitor"].dirty);
surfaces.set_mode("monitor", RenderMode::Snapshot); assert!(surfaces.entries["monitor"].dirty);
surfaces.entries.get_mut("monitor").unwrap().dirty = false;
surfaces.invalidate("monitor");
assert!(surfaces.entries["monitor"].dirty);
}
#[test]
fn bind_surfaces_binds_registered_and_hides_unregistered() {
let mut app = test_app();
app.add_systems(Update, bind_surfaces);
let root = app
.world_mut()
.spawn((RSurface("monitor".into()), Visibility::Inherited))
.id();
app.update();
assert!(app.world().entity(root).get::<UiTargetCamera>().is_none());
assert_eq!(
app.world().entity(root).get::<Visibility>().copied(),
Some(Visibility::Hidden),
"an unregistered surface root is hidden"
);
app.world_mut()
.resource_scope(|world, mut surfaces: Mut<Surfaces>| {
let mut images = world.resource_mut::<Assets<Image>>();
surfaces.create(&mut images, "monitor", SurfaceSpec::default());
});
app.update();
let cam = app
.world()
.entity(root)
.get::<UiTargetCamera>()
.map(|t| t.0)
.expect("root binds to its surface camera");
assert!(
app.world().entity(cam).get::<SurfaceCamera>().is_some(),
"the bound camera is a surface camera"
);
assert_eq!(
app.world().entity(root).get::<Visibility>().copied(),
Some(Visibility::Inherited),
"a bound surface root is shown"
);
}
}