diff --git a/Cargo.toml b/Cargo.toml index bd775dc..3ff46eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,15 @@ [package] name = "bevy_smooth_pixel_camera" -version = "0.1.0" +version = "0.12.0" edition = "2021" authors = ["Doonv"] description = "Smooth pixel-perfect camera for Bevy" repository = "https://github.com/doonv/bevy_smooth_pixel_camera" -exclude = ["assets/"] +exclude = ["assets/", ".github/"] keywords = ["game", "gamedev", "graphics", "bevy", "pixel", "pixel-perfect"] license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bevy = "0.11.3" - -# Enable a small amount of optimization in debug mode -[profile.dev] -opt-level = 1 - -# Enable high optimizations for dependencies (incl. Bevy), but not for our code: -[profile.dev.package."*"] -opt-level = 3 +bevy = "0.12.0" diff --git a/README.md b/README.md index 05a39f9..8b41d10 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ cargo add bevy_smooth_pixel_camera ``` 2. Add the `PixelCameraPlugin` and set the `ImagePlugin` to `default_nearest`. ```rs -app.add_plugins( +app.add_plugins(( DefaultPlugins.set(ImagePlugin::default_nearest()), PixelCameraPlugin -) +)); ``` 3. Add a pixel pefect camera to your scene. ```rs fn setup(mut commands: Commands) { commands.spawn(( Camera2dBundle::default(), - PixelCamera::from_scale(4) + PixelCamera::from_scaling(4) )); } ``` diff --git a/assets/.checkerboard.png-autosave.kra b/assets/.checkerboard.png-autosave.kra deleted file mode 100644 index 15bd772..0000000 Binary files a/assets/.checkerboard.png-autosave.kra and /dev/null differ diff --git a/examples/basic.rs b/examples/basic.rs index 0b3568f..fd1a387 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use bevy_smooth_pixel_camera::*; +use bevy_smooth_pixel_camera::prelude::*; #[derive(Component)] struct BevyIcon; diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..334d1f4 --- /dev/null +++ b/src/components.rs @@ -0,0 +1,52 @@ +use bevy::{prelude::*, render::view::RenderLayers}; + +/// The pixelated camera component. +/// +/// Add this component to a [`Camera2dBundle`] in order to turn it into a +/// pixelated camera. +/// +/// **Warning:** In order to move the camera please use the `subpixel_pos` +/// attribute instead of the [`Transform`] component (the transform is a truncated version of subpixel_pos) +#[derive(Component)] +pub struct PixelCamera { + /// The level of upscaling to use for pixels. + /// + /// For example: A scaling of `4` which cause every world pixel to be 4x4 in size on the screen. + pub scaling: u8, + /// The subpixel position of the [`PixelCamera`], use this instead of the camera's [`Transform`]. + pub subpixel_pos: Vec2, + /// The order in which the viewport camera renders. + /// Cameras with a higher order are rendered later, and thus on top of lower order cameras. + /// + /// Because we want the world camera to render before the viewport camera, set this value to a positive number. + pub viewport_order: isize, + /// The rendering layer the viewport is on. + pub viewport_layer: RenderLayers, +} + +impl Default for PixelCamera { + fn default() -> Self { + Self { + viewport_order: 1, + scaling: 2, + viewport_layer: RenderLayers::layer(1), + subpixel_pos: Vec2::ZERO, + } + } +} + +impl PixelCamera { + /// Creates a new pixel camera with the `scaling` of choice. + pub fn from_scaling(scaling: u8) -> Self { + Self { + scaling, + ..default() + } + } +} + + +#[derive(Component)] +pub struct PixelViewport(pub Entity); +#[derive(Component)] +pub struct PixelViewportMarker; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0044555..f75f3cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,80 +11,32 @@ //! 2. Add the `PixelCameraPlugin` and set the `ImagePlugin` to `default_nearest`. //! ``` //! use bevy::prelude::*; +//! use bevy_smooth_pixel_camera::PixelCameraPlugin; //! -//! App::new().add_plugins( +//! App::new().add_plugins(( //! DefaultPlugins.set(ImagePlugin::default_nearest()), //! PixelCameraPlugin -//! ) +//! )); //! ``` //! 3. Add a pixel pefect camera to your scene. //! ``` //! use bevy::prelude::*; +//! use bevy_smooth_pixel_camera::PixelCamera; //! //! fn setup(mut commands: Commands) { //! commands.spawn(( //! Camera2dBundle::default(), -//! PixelCamera::from_scale(4) +//! PixelCamera::from_scaling(4) //! )); //! } //! ``` //! 4. That should be it! -use bevy::{ - prelude::*, - render::{camera::RenderTarget, render_resource::*, view::RenderLayers}, - window::WindowResolution, -}; +use bevy::{prelude::*, render::render_resource::*, window::WindowResolution}; -/// The pixelated camera component. -/// -/// Add this component to a [`Camera2dBundle`] in order to turn it into a -/// pixelated camera. -/// -/// **Warning:** In order to move the camera please use the `subpixel_pos` -/// attribute instead of the [`Transform`] component (the transform is a truncated version of subpixel_pos) -#[derive(Component)] -pub struct PixelCamera { - /// The level of upscaling to use for pixels. - /// - /// For example: A scaling of `4` which cause every world pixel to be 4x4 in size on the screen. - pub scaling: u8, - /// The subpixel position of the [`PixelCamera`], use this instead of the camera's [`Transform`]. - pub subpixel_pos: Vec2, - /// The order in which the viewport camera renders. - /// Cameras with a higher order are rendered later, and thus on top of lower order cameras. - /// - /// Because we want the world camera to render before the viewport camera, set this value to a positive number. - pub viewport_order: isize, - /// The rendering layer the viewport is on. - pub viewport_layer: RenderLayers, -} - -#[derive(Component)] -struct PixelViewport(Entity); -#[derive(Component)] -struct PixelViewportMarker; - -impl Default for PixelCamera { - fn default() -> Self { - Self { - viewport_order: 1, - scaling: 2, - viewport_layer: RenderLayers::layer(1), - subpixel_pos: Vec2::ZERO, - } - } -} - -impl PixelCamera { - /// Creates a new pixel camera with the `scaling` of choice. - pub fn from_scaling(scaling: u8) -> Self { - Self { - scaling, - ..default() - } - } -} +pub mod components; +pub mod prelude; +pub mod systems; /// The [`PixelCameraPlugin`] handles initialization and updates of the [`PixelCamera`]. /// @@ -92,170 +44,21 @@ impl PixelCamera { pub struct PixelCameraPlugin; impl Plugin for PixelCameraPlugin { fn build(&self, app: &mut App) { - app.insert_resource(Msaa::Off) - .add_systems(Update, (init_camera, update_viewport_size, smooth_camera)); + app.insert_resource(Msaa::Off).add_systems( + Update, + ( + systems::init_camera, + systems::update_viewport_size, + systems::smooth_camera, + ), + ); } } -fn get_viewport_size(window_resolution: &WindowResolution, scaling: u8) -> Extent3d { +pub fn get_viewport_size(window_resolution: &WindowResolution, scaling: u8) -> Extent3d { Extent3d { width: (window_resolution.width() / scaling as f32).ceil() as u32 + 2, height: (window_resolution.height() / scaling as f32).ceil() as u32 + 2, depth_or_array_layers: 1, } } - -fn init_camera( - mut query: Query< - (&PixelCamera, &mut Camera, Option<&RenderLayers>, Entity), - Added, - >, - window_query: Query<&Window>, - mut images: ResMut>, - mut commands: Commands, -) { - let window = window_query.single(); - - for ( - PixelCamera { - viewport_order, - scaling, - viewport_layer, - .. - }, - mut camera, - world_layer, - entity, - ) in query.iter_mut() - { - if let Some(world_layer) = world_layer { - if world_layer.intersects(viewport_layer) { - error!("The render layers of the world intersect with the render layers of the viewport camera"); - return; - } - } else if viewport_layer.intersects(&RenderLayers::layer(0)) { - error!("The render layers of the viewport camera intersect with the default render layer of the world"); - return; - } else if *viewport_layer == RenderLayers::none() { - error!("The viewport camera has no render layers and will be rendered on the world"); - return; - } - - if &camera.order >= viewport_order { - error!("The camera is configured to render later or at the same time as of the viewport camera. (camera.order >= viewport_camera.order)"); - return; - } - - let size = get_viewport_size(&window.resolution, *scaling); - - // This is the texture that will be rendered to. - let mut image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Bgra8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, - ..default() - }; - - // fill image.data with zeroes - image.resize(size); - - let image_handle = images.add(image); - - camera.target = RenderTarget::Image(image_handle.clone()); - - let viewport_entity = commands - .spawn(( - SpriteBundle { - texture: image_handle.clone(), - transform: Transform::from_scale(Vec2::splat(*scaling as f32).extend(1.0)), - ..default() - }, - *viewport_layer, - PixelViewportMarker, - )) - .id(); - - commands.spawn(( - Camera2dBundle { - camera: Camera { - order: *viewport_order, - ..default() - }, - ..default() - }, - *viewport_layer, - )); - - commands - .entity(entity) - .insert(PixelViewport(viewport_entity)); - } -} - -fn update_viewport_size( - mut query: Query<(&PixelCamera, &mut Camera)>, - window_query: Query<&Window, Changed>, - mut images: ResMut>, -) { - let window = if let Ok(window) = window_query.get_single() { - window - } else { - return; - }; - - for (PixelCamera { scaling, .. }, mut camera) in query.iter_mut() { - if let RenderTarget::Image(image_handle) = &mut camera.target { - let image = images.get_mut(image_handle); - - if let Some(image) = image { - let new_size = get_viewport_size(&window.resolution, *scaling); - - image.resize(new_size); - } else { - error!("Pixel camera render target image doesn't exist!"); - } - } - } -} - -fn smooth_camera( - mut query: Query<(&PixelCamera, &mut Transform, &PixelViewport)>, - mut viewports: Query<&mut Transform, (With, Without)>, -) { - for ( - PixelCamera { - scaling, - subpixel_pos, - .. - }, - mut camera_transform, - viewport, - ) in query.iter_mut() - { - let mut viewport_transform = viewports.get_mut(viewport.0).unwrap(); - let scaling_f32 = *scaling as f32; - - // Set the camera transform the rounded down version of the subpixel position - camera_transform.translation.x = subpixel_pos.x.trunc(); - camera_transform.translation.y = subpixel_pos.y.trunc(); - - // In order to get smooth camera movement while retaining pixel perfection, - // we can move the viewport's transform by the remainder of the subpixel. - // - // The smoothing is based on this video: https://youtu.be/jguyR4yJb1M?t=98 - let remainder_x = subpixel_pos.x % 1.; - let remainder_y = subpixel_pos.y % 1.; - - viewport_transform.translation.x = -remainder_x * scaling_f32; - viewport_transform.translation.y = -remainder_y * scaling_f32; - } -} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..1facc23 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,3 @@ +//! `use bevy_smooth_pixel_camera::prelude::*;` to import the [`PixelCamera`] and [`PixelCameraPlugin`]. + +pub use super::{PixelCameraPlugin, components::PixelCamera}; diff --git a/src/systems.rs b/src/systems.rs new file mode 100644 index 0000000..d7e9ee3 --- /dev/null +++ b/src/systems.rs @@ -0,0 +1,159 @@ +use bevy::{prelude::*, render::{render_resource::*, view::RenderLayers, camera::RenderTarget}}; + +use crate::{components::*, get_viewport_size}; + +pub fn init_camera( + mut query: Query< + (&PixelCamera, &mut Camera, Option<&RenderLayers>, Entity), + Added, + >, + window_query: Query<&Window>, + mut images: ResMut>, + mut commands: Commands, +) { + let window = window_query.single(); + + for ( + PixelCamera { + viewport_order, + scaling, + viewport_layer, + .. + }, + mut camera, + world_layer, + entity, + ) in query.iter_mut() + { + if let Some(world_layer) = world_layer { + if world_layer.intersects(viewport_layer) { + error!("The render layers of the world intersect with the render layers of the viewport camera"); + return; + } + } else if viewport_layer.intersects(&RenderLayers::layer(0)) { + error!("The render layers of the viewport camera intersect with the default render layer of the world"); + return; + } else if *viewport_layer == RenderLayers::none() { + error!("The viewport camera has no render layers and will be rendered on the world"); + return; + } + + if &camera.order >= viewport_order { + error!("The camera is configured to render later or at the same time as of the viewport camera. (camera.order >= viewport_camera.order)"); + return; + } + + let size = get_viewport_size(&window.resolution, *scaling); + + // This is the texture that will be rendered to. + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Bgra8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + + // fill image.data with zeroes + image.resize(size); + + let image_handle = images.add(image); + + camera.target = RenderTarget::Image(image_handle.clone()); + + let viewport_entity = commands + .spawn(( + SpriteBundle { + texture: image_handle.clone(), + transform: Transform::from_scale(Vec2::splat(*scaling as f32).extend(1.0)), + ..default() + }, + *viewport_layer, + PixelViewportMarker, + )) + .id(); + + commands.spawn(( + Camera2dBundle { + camera: Camera { + order: *viewport_order, + ..default() + }, + ..default() + }, + *viewport_layer, + )); + + commands + .entity(entity) + .insert(PixelViewport(viewport_entity)); + } +} + +pub fn smooth_camera( + mut query: Query<(&PixelCamera, &mut Transform, &PixelViewport)>, + mut viewports: Query<&mut Transform, (With, Without)>, +) { + for ( + PixelCamera { + scaling, + subpixel_pos, + .. + }, + mut camera_transform, + viewport, + ) in query.iter_mut() + { + let mut viewport_transform = viewports.get_mut(viewport.0).unwrap(); + let scaling_f32 = *scaling as f32; + + // Set the camera transform the rounded down version of the subpixel position + camera_transform.translation.x = subpixel_pos.x.trunc(); + camera_transform.translation.y = subpixel_pos.y.trunc(); + + // In order to get smooth camera movement while retaining pixel perfection, + // we can move the viewport's transform by the remainder of the subpixel. + // + // The smoothing is based on this video: https://youtu.be/jguyR4yJb1M?t=98 + let remainder_x = subpixel_pos.x % 1.; + let remainder_y = subpixel_pos.y % 1.; + + viewport_transform.translation.x = -remainder_x * scaling_f32; + viewport_transform.translation.y = -remainder_y * scaling_f32; + } +} + +pub fn update_viewport_size( + mut query: Query<(&PixelCamera, &mut Camera)>, + window_query: Query<&Window, Changed>, + mut images: ResMut>, +) { + let window = if let Ok(window) = window_query.get_single() { + window + } else { + return; + }; + + for (PixelCamera { scaling, .. }, mut camera) in query.iter_mut() { + if let RenderTarget::Image(image_handle) = &mut camera.target { + // TODO: Remove the `.id()` part once https://github.com/bevyengine/bevy/pull/10372 gets merged + let image = images.get_mut(image_handle.id()); + + if let Some(image) = image { + let new_size = get_viewport_size(&window.resolution, *scaling); + + image.resize(new_size); + } else { + error!("Pixel camera render target image doesn't exist!"); + } + } + } +}