├── assets └── mirror.png ├── .gitignore ├── src ├── shaders │ └── portal.wgsl ├── gizmos.rs ├── lib.rs ├── material.rs ├── picking.rs └── camera.rs ├── LICENSE-MIT ├── Cargo.toml ├── examples ├── basic.rs ├── mirror.rs ├── mesh_picking.rs └── teleport.rs ├── README.md └── LICENSE-APACHE /assets/mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chompaa/bevy_easy_portals/HEAD/assets/mirror.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust builds 2 | /target 3 | # This file contains environment-specific configuration like linker settings 4 | .cargo/config.toml 5 | -------------------------------------------------------------------------------- /src/shaders/portal.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::{ 2 | forward_io::VertexOutput, 3 | mesh_view_bindings::view, 4 | utils::coords_to_viewport_uv, 5 | } 6 | 7 | @group(#{MATERIAL_BIND_GROUP}) @binding(0) var base_color_texture: texture_2d; 8 | @group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_color_sampler: sampler; 9 | 10 | @fragment 11 | fn fragment(mesh: VertexOutput) -> @location(0) vec4 { 12 | let viewport_uv = coords_to_viewport_uv(mesh.position.xy, view.viewport); 13 | return textureSample(base_color_texture, base_color_sampler, viewport_uv); 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_easy_portals" 3 | version = "0.6.0" 4 | edition = "2024" 5 | description = "Bevy plugin for easy-to-use portals." 6 | keywords = ["bevy", "gamedev"] 7 | categories = ["game-development"] 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | repository = "https://github.com/chompaa/bevy_easy_portals" 11 | documentation = "https://docs.rs/bevy_easy_portals" 12 | exclude = [".github"] 13 | 14 | [dependencies] 15 | uuid = { version = "1.11.0", features = ["v4"], optional = true } 16 | 17 | [dependencies.bevy] 18 | version = "0.17" 19 | default-features = false 20 | features = [ 21 | "bevy_core_pipeline", 22 | "bevy_image", 23 | "bevy_pbr", 24 | "bevy_render", 25 | "bevy_window", 26 | "bevy_log", 27 | ] 28 | 29 | [dev-dependencies.bevy] 30 | version = "0.17" 31 | 32 | [features] 33 | default = [] 34 | gizmos = ["bevy/bevy_gizmos"] 35 | picking = ["bevy/bevy_picking", "dep:uuid"] 36 | 37 | [lints.clippy] 38 | too_many_arguments = "allow" 39 | type_complexity = "allow" 40 | 41 | [package.metadata.docs.rs] 42 | all-features = true 43 | 44 | [[example]] 45 | name = "basic" 46 | 47 | [[example]] 48 | name = "mesh_picking" 49 | required-features = ["picking"] 50 | 51 | [[example]] 52 | name = "mirror" 53 | 54 | [[example]] 55 | name = "teleport" 56 | -------------------------------------------------------------------------------- /src/gizmos.rs: -------------------------------------------------------------------------------- 1 | //! Gizmos for [`Portal`] debugging. 2 | 3 | use bevy::{camera::primitives::Aabb, color::palettes::tailwind::ORANGE_600, prelude::*}; 4 | 5 | use crate::Portal; 6 | 7 | #[derive(Reflect, Default, GizmoConfigGroup)] 8 | pub struct PortalGizmos; 9 | 10 | /// Gizmo plugin for [`Portal`] debugging. 11 | /// 12 | /// These gizmos help visualize aspects like [`Portal`] meshes and where the 13 | /// [`Portal::target_transform`] is located (along with its facing direction). 14 | pub struct PortalGizmosPlugin; 15 | 16 | impl Plugin for PortalGizmosPlugin { 17 | fn build(&self, app: &mut App) { 18 | app.init_gizmo_group::() 19 | .add_systems(Update, (debug_portal_meshes, debug_portal_cameras)); 20 | } 21 | } 22 | 23 | /// System that renders the [`Aabb`]s of a [`Portal`]'s mesh. 24 | fn debug_portal_meshes( 25 | mut gizmos: Gizmos, 26 | portal_query: Query<(&GlobalTransform, &Aabb), With>, 27 | ) { 28 | for (&global_transform, aabb) in &portal_query { 29 | let transform = Transform { 30 | translation: global_transform.translation(), 31 | rotation: global_transform.rotation(), 32 | scale: (aabb.half_extents * 2.0).into(), 33 | }; 34 | gizmos.cuboid(transform, ORANGE_600); 35 | } 36 | } 37 | 38 | /// System that renders arrows indicating the translation and rotation of [`PortalCamera`]s. 39 | fn debug_portal_cameras( 40 | mut gizmos: Gizmos, 41 | portal_query: Query<&Portal>, 42 | global_transform_query: Query<&GlobalTransform>, 43 | ) { 44 | for portal in &portal_query { 45 | let target_transform = global_transform_query 46 | .get(portal.target) 47 | .map(GlobalTransform::compute_transform) 48 | .expect("target should have GlobalTransform"); 49 | let start_target = target_transform.translation; 50 | let end_target = start_target + target_transform.forward() * 0.5; 51 | gizmos.arrow(start_target, end_target, ORANGE_600); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy::{color::palettes::tailwind::ORANGE_600, prelude::*}; 2 | #[cfg(feature = "gizmos")] 3 | use bevy_easy_portals::gizmos::PortalGizmosPlugin; 4 | use bevy_easy_portals::{Portal, PortalPlugins}; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins(( 9 | DefaultPlugins, 10 | PortalPlugins, 11 | #[cfg(feature = "gizmos")] 12 | PortalGizmosPlugin, 13 | )) 14 | .add_systems(Startup, setup) 15 | .run(); 16 | } 17 | 18 | fn setup( 19 | mut commands: Commands, 20 | mut meshes: ResMut>, 21 | mut materials: ResMut>, 22 | ) { 23 | let primary_camera = commands 24 | .spawn(( 25 | Camera3d::default(), 26 | Camera { 27 | clear_color: ClearColorConfig::Custom(Color::BLACK), 28 | ..default() 29 | }, 30 | Transform::from_xyz(-3.5, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), 31 | )) 32 | .id(); 33 | 34 | commands.insert_resource(AmbientLight { 35 | brightness: 750.0, 36 | ..default() 37 | }); 38 | 39 | let shape = commands 40 | .spawn(( 41 | Mesh3d(meshes.add(Cuboid::default())), 42 | MeshMaterial3d(materials.add(Color::from(ORANGE_600))), 43 | Transform::from_xyz(1.5, 0.0, 0.0), 44 | )) 45 | .id(); 46 | 47 | let target_transform = Transform::from_xyz(0.0, 0.0, 2.0); 48 | let target = commands.spawn(target_transform).id(); 49 | 50 | // We'll set the target relative to our shape, since that's what we want to look at 51 | commands.entity(shape).add_child(target); 52 | 53 | let rectangle = Rectangle::from_size(Vec2::splat(2.5)); 54 | let portal_transform = Transform::from_xyz(-1.5, 0.0, 0.0); 55 | commands 56 | .spawn(( 57 | // No need to spawn a material for the mesh here, it will be taken care of by the 58 | // portal setup 59 | Mesh3d(meshes.add(rectangle)), 60 | portal_transform, 61 | Portal::new(primary_camera, target), 62 | )) 63 | .with_children(|parent| { 64 | // We can use another mesh for our portal if we wish 65 | parent.spawn(( 66 | Mesh3d(meshes.add(rectangle)), 67 | MeshMaterial3d(materials.add(Color::WHITE.with_alpha(0.05))), 68 | )); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `bevy_easy_portals` 2 | 3 | Easy-to-use portals for Bevy 4 | 5 | ![screenshot showing a cube being reflected in a mirror using portals](https://raw.githubusercontent.com/chompaa/bevy_easy_portals/main/assets/mirror.png) 6 | 7 | ## Getting Started 8 | 9 | First, add `PortalPlugin` to your app, then use the `Portal` component, et voila! 10 | 11 | See [the examples](https://github.com/chompaa/bevy_easy_portals/tree/main/examples) for more references. 12 | 13 |
14 | 15 | Example usage 16 | 17 | ```rust 18 | use bevy::prelude::*; 19 | use bevy_easy_portals::{Portal, PortalPlugins} 20 | 21 | fn main() { 22 | App::new() 23 | .add_plugins((DefaultPlugins, PortalPlugins)) 24 | .add_systems(Startup, setup) 25 | .run(); 26 | } 27 | 28 | fn setup( 29 | mut commands: Commands, 30 | mut materials: ResMut>, 31 | mut meshes: ResMut>, 32 | ) { 33 | let primary_camera = commands 34 | .spawn((Camera3d::default(), Transform::from_xyz(0.0, 0.0, 10.0))) 35 | .id(); 36 | 37 | // Spawn something for the portal to look at 38 | commands.spawn(( 39 | Mesh3d(meshes.add(Cuboid::default())), 40 | MeshMaterial3d(materials.add(Color::WHITE)), 41 | Transform::from_xyz(10.0, 0.0, 0.0), 42 | )); 43 | 44 | // Where the portal's target camera should be 45 | let target = commands.spawn(Transform::from_xyz(10.0, 0.0, 10.0)).id(); 46 | // Where the portal should be located 47 | let portal_transform = Transform::default(); 48 | // Spawn the portal, omit a material since one will be added automatically 49 | commands.spawn(( 50 | Mesh3d(meshes.add(Rectangle::default())), 51 | portal_transform, 52 | Portal::new(primary_camera, target), 53 | )); 54 | } 55 | ``` 56 | 57 |
58 | 59 | ## Compatibility 60 | 61 | | `bevy_easy_portals` | `bevy` | 62 | | :-- | :-- | 63 | | `0.6` | `0.17` | 64 | | `0.5` | `0.16` | 65 | | `0.3..0.4` | `0.15` | 66 | | `0.1..0.2` | `0.14` | 67 | 68 | ## Features 69 | 70 | | Feature | Description | 71 | | :-- | :-- | 72 | | `picking` | Support picking through portals with using your favorite backend | 73 | | `gizmos` | Use gizmos for the portal's aabb and camera transform | 74 | 75 | ## Contributing 76 | 77 | Feel free to open a PR! 78 | 79 | If possible, please try to keep it minimal and scoped. 80 | 81 | ## Alternatives 82 | 83 | - [`bevy_basic_portals`](https://github.com/Selene-Amanita/bevy_basic_portals) 84 | -------------------------------------------------------------------------------- /examples/mirror.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | 3 | use bevy::{color::palettes::tailwind::ORANGE_600, prelude::*}; 4 | #[cfg(feature = "gizmos")] 5 | use bevy_easy_portals::gizmos::PortalGizmosPlugin; 6 | use bevy_easy_portals::{Portal, PortalPlugins}; 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins(( 11 | DefaultPlugins, 12 | PortalPlugins, 13 | #[cfg(feature = "gizmos")] 14 | PortalGizmosPlugin, 15 | )) 16 | .add_systems(Startup, setup) 17 | .add_systems(Update, rotate_shape) 18 | .run(); 19 | } 20 | 21 | #[derive(Component)] 22 | struct Shape; 23 | 24 | fn setup( 25 | mut commands: Commands, 26 | mut meshes: ResMut>, 27 | mut materials: ResMut>, 28 | ) { 29 | // It's important we keep track of this entity, since the portal with require it 30 | let primary_camera = commands 31 | .spawn(( 32 | Camera3d::default(), 33 | Camera { 34 | // The portal will inherit properties of the primary camera 35 | clear_color: ClearColorConfig::Custom(Color::BLACK), 36 | ..default() 37 | }, 38 | Transform::from_xyz(2.5, 0.0, 9.0).looking_at(Vec3::ZERO, Vec3::Y), 39 | )) 40 | .id(); 41 | 42 | // Spawn a shape so we can see something in the reflection 43 | let shape_transform = Transform::from_xyz(0.0, 0.0, 4.0); 44 | commands.spawn(( 45 | Mesh3d(meshes.add(Cuboid::default())), 46 | MeshMaterial3d(materials.add(Color::from(ORANGE_600))), 47 | shape_transform, 48 | Shape, 49 | )); 50 | commands.spawn(( 51 | PointLight { 52 | intensity: 10_000_000.0, 53 | ..default() 54 | }, 55 | Transform::from_xyz(0.0, 10.0, 0.0).looking_at(shape_transform.translation, Vec3::Y), 56 | )); 57 | 58 | let rectangle = Rectangle::from_size(Vec2::splat(5.0)); 59 | 60 | let mirror = commands 61 | .spawn(( 62 | // No need to spawn a material for the mesh here, it will be taken care of by the 63 | // portal setup 64 | Mesh3d(meshes.add(rectangle)), 65 | Transform::from_xyz(0.0, 0.0, 0.0), 66 | )) 67 | .with_children(|parent| { 68 | // We can use another mesh for our mirror if we wish 69 | parent.spawn(( 70 | Mesh3d(meshes.add(rectangle)), 71 | MeshMaterial3d(materials.add(Color::WHITE.with_alpha(0.2))), 72 | )); 73 | }) 74 | .id(); 75 | 76 | // The target should be the transform of the mirror itself, but flipped 77 | let target_transform = Transform::default().with_rotation(Quat::from_rotation_y(PI)); 78 | let target = commands.spawn(target_transform).id(); 79 | 80 | commands 81 | .entity(mirror) 82 | // Since we're constructing a mirror, let's parent the target to the mirror itself 83 | .add_child(target) 84 | // Now let's create the portal! 85 | .insert(Portal::new(primary_camera, target)); 86 | } 87 | 88 | fn rotate_shape(mut shape_transform: Single<&mut Transform, With>, time: Res