#![doc = include_str!("../README.md")]
#[cfg(feature = "gizmos")]
pub mod gizmos;
use bevy::{
core_pipeline::tonemapping::{DebandDither, Tonemapping},
ecs::system::SystemParam,
image::{TextureFormatPixelInfo, Volume},
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
render::{
camera::{Exposure, RenderTarget},
mesh::MeshVertexBufferLayoutRef,
primitives::{Frustum, HalfSpace},
render_resource::{
AsBindGroup, Extent3d, Face, RenderPipelineDescriptor, ShaderRef,
SpecializedMeshPipelineError, TextureDescriptor, TextureDimension, TextureFormat,
TextureUsages,
},
view::{ColorGrading, VisibilitySystems},
},
window::{PrimaryWindow, WindowRef, WindowResized},
};
const PORTAL_SHADER_PATH: &str = "portal.wgsl";
#[derive(Default)]
pub struct PortalPlugin;
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
pub enum PortalCameraSystems {
ResizeImage,
UpdateTransform,
UpdateFrusta,
}
impl Plugin for PortalPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<PortalMaterial>::default())
.add_systems(
PreUpdate,
resize_portal_images.in_set(PortalCameraSystems::ResizeImage),
)
.add_systems(
PostUpdate,
(
update_portal_camera_transform.in_set(PortalCameraSystems::UpdateTransform),
update_portal_camera_frusta.in_set(PortalCameraSystems::UpdateFrusta),
)
.after(TransformSystem::TransformPropagate)
.before(VisibilitySystems::UpdateFrusta)
.chain(),
)
.add_observer(setup_portal)
.register_type::<(Portal, PortalCamera)>();
}
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
#[require(Transform)]
pub struct Portal {
pub primary_camera: Entity,
pub target: Entity,
#[reflect(ignore)]
pub cull_mode: Option<Face>,
pub linked_camera: Option<Entity>,
}
impl Portal {
#[inline]
#[must_use]
pub fn new(primary_camera: Entity, target: Entity) -> Self {
Self {
primary_camera,
target,
cull_mode: Some(Face::Back),
linked_camera: None,
}
}
#[inline]
#[must_use]
pub fn with_cull_mode(mut self, cull_mode: Option<Face>) -> Self {
self.cull_mode = cull_mode;
self
}
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
#[require(Camera3d)]
pub struct PortalCamera;
#[derive(Asset, AsBindGroup, Clone, TypePath)]
#[bind_group_data(PortalMaterialKey)]
pub struct PortalMaterial {
#[texture(0)]
#[sampler(1)]
base_color_texture: Option<Handle<Image>>,
pub cull_mode: Option<Face>,
}
impl Material for PortalMaterial {
fn fragment_shader() -> ShaderRef {
PORTAL_SHADER_PATH.into()
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.primitive.cull_mode = key.bind_group_data.cull_mode;
Ok(())
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PortalMaterialKey {
cull_mode: Option<Face>,
}
impl From<&PortalMaterial> for PortalMaterialKey {
fn from(material: &PortalMaterial) -> Self {
Self {
cull_mode: material.cull_mode,
}
}
}
fn setup_portal(
trigger: Trigger<OnAdd, Portal>,
mut commands: Commands,
mut portal_query: Query<&mut Portal>,
primary_camera_query: Query<(
&Camera,
Option<&Camera3d>,
Option<&DebandDither>,
Option<&Tonemapping>,
Option<&ColorGrading>,
Option<&Exposure>,
)>,
mut images: ResMut<Assets<Image>>,
mut portal_materials: ResMut<Assets<PortalMaterial>>,
global_transform_query: Query<&GlobalTransform>,
viewport_size: ViewportSize,
) {
let entity = trigger.entity();
let mut portal = portal_query
.get_mut(entity)
.expect("observer guarantees existence of component");
let Ok((primary_camera, camera_3d, tonemapping, deband_dither, color_grading, exposure)) =
primary_camera_query.get(portal.primary_camera)
else {
error!(
"could not setup portal {entity}: primary_camera does not contain a Camera component"
);
return;
};
let image_handle = {
let Some(size) = viewport_size.get_viewport_size(primary_camera) else {
error!("could not compute viewport size for portal {entity}");
return;
};
let format = TextureFormat::Bgra8UnormSrgb;
let image = Image {
data: vec![0; size.volume() * format.pixel_size()],
texture_descriptor: TextureDescriptor {
label: None,
size,
dimension: TextureDimension::D2,
format,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
..default()
};
images.add(image)
};
let Ok(global_transform) = global_transform_query.get(portal.target).copied() else {
error!("portal target is missing a GlobalTransform");
return;
};
portal.linked_camera = Some(
commands
.spawn((
Name::new("Portal Camera"),
Camera {
order: -1,
target: RenderTarget::Image(image_handle.clone()),
..primary_camera.clone()
},
global_transform.compute_transform(),
global_transform,
camera_3d.cloned().unwrap_or_default(),
tonemapping.copied().unwrap_or_default(),
deband_dither.copied().unwrap_or_default(),
color_grading.cloned().unwrap_or_default(),
exposure.copied().unwrap_or_default(),
PortalCamera,
))
.id(),
);
commands
.entity(entity)
.insert(MeshMaterial3d(portal_materials.add(PortalMaterial {
base_color_texture: Some(image_handle.clone()),
cull_mode: portal.cull_mode,
})));
}
fn update_portal_camera_transform(
primary_camera_transform_query: Query<
&GlobalTransform,
(With<Camera3d>, Without<PortalCamera>),
>,
portal_query: Query<(&GlobalTransform, &Portal), (Without<Camera3d>, Without<PortalCamera>)>,
mut portal_camera_transform_query: Query<
(&mut GlobalTransform, &mut Transform),
With<PortalCamera>,
>,
target_global_transform_query: Query<
&GlobalTransform,
(Without<Camera3d>, Without<PortalCamera>, Without<Portal>),
>,
) {
for (portal_global_transform, portal) in &portal_query {
let Ok(primary_camera_transform) = primary_camera_transform_query
.get(portal.primary_camera)
.map(GlobalTransform::compute_transform)
else {
continue;
};
let Some(linked_camera) = portal.linked_camera else {
continue;
};
let (mut portal_camera_global_transform, mut portal_camera_transform) =
portal_camera_transform_query
.get_mut(linked_camera)
.unwrap();
let portal_transform = portal_global_transform.compute_transform();
let target_transform = target_global_transform_query
.get(portal.target)
.unwrap()
.compute_transform();
let translation = primary_camera_transform.translation - portal_transform.translation
+ target_transform.translation;
let rotation = portal_transform
.rotation
.inverse()
.mul_quat(target_transform.rotation);
*portal_camera_transform = primary_camera_transform.with_translation(translation);
portal_camera_transform.rotate_around(target_transform.translation, rotation);
*portal_camera_global_transform = GlobalTransform::from(*portal_camera_transform);
}
}
fn update_portal_camera_frusta(
portal_query: Query<&Portal>,
mut frustum_query: Query<&mut Frustum, With<PortalCamera>>,
global_transform_query: Query<&GlobalTransform>,
) {
for portal in &portal_query {
let Some(linked_camera) = portal.linked_camera else {
continue;
};
let mut frustum = frustum_query.get_mut(linked_camera).unwrap();
let (target_transform, portal_camera_transform) = global_transform_query
.get_many([portal.target, linked_camera])
.map(|[t, c]| (t.compute_transform(), c.compute_transform()))
.unwrap();
let normal = -target_transform.forward().normalize_or_zero();
let distance =
-((target_transform.translation - portal_camera_transform.translation).dot(normal));
frustum.half_spaces[4] = HalfSpace::new(normal.extend(distance));
}
}
fn resize_portal_images(
mut resized_reader: EventReader<WindowResized>,
window_query: Query<&Window>,
portal_query: Query<(&Portal, &MeshMaterial3d<PortalMaterial>)>,
camera_query: Query<&Camera>,
mut images: ResMut<Assets<Image>>,
mut portal_materials: ResMut<Assets<PortalMaterial>>,
) {
for event in resized_reader.read() {
let window_size = window_query.get(event.window).unwrap().physical_size();
let size = Extent3d {
width: window_size.x,
height: window_size.y,
..default()
};
for (portal, portal_material_handle) in &portal_query {
let Some(camera) = portal.linked_camera.and_then(|c| camera_query.get(c).ok()) else {
continue;
};
let RenderTarget::Image(ref image_handle) = camera.target else {
continue;
};
let Some(image) = images.get_mut(image_handle) else {
continue;
};
image.resize(size);
portal_materials.get_mut(portal_material_handle);
}
}
}
#[derive(SystemParam)]
struct ViewportSize<'w, 's> {
primary_window_query: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
window_query: Query<'w, 's, &'static Window>,
}
impl ViewportSize<'_, '_> {
fn get_viewport_size(&self, camera: &Camera) -> Option<Extent3d> {
match camera.viewport.as_ref() {
Some(viewport) => Some(viewport.physical_size),
None => match &camera.target {
RenderTarget::Window(window_ref) => (match window_ref {
WindowRef::Primary => self.primary_window_query.get_single().ok(),
WindowRef::Entity(entity) => self.window_query.get(*entity).ok(),
})
.map(Window::physical_size),
_ => None,
},
}
.map(|size| Extent3d {
width: size.x,
height: size.y,
..default()
})
}
}