bevy_smooth_pixel_camera 0.4.1

Smooth pixel-perfect camera for Bevy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
//! The components of [`bevy_smooth_pixel_camera`](crate).

use bevy::asset::RenderAssetUsages;
use bevy::camera::RenderTarget;
use bevy::camera::visibility::RenderLayers;
use bevy::ecs::entity_disabling::Disabled;
use bevy::ecs::lifecycle::HookContext;
#[cfg(feature = "picking")]
use bevy::picking::pointer::PointerLocation;
use bevy::prelude::*;
use bevy::render::render_resource::{TextureFormat, TextureUsages};
use bevy::window::PrimaryWindow;
use std::mem;

#[cfg(feature = "picking")]
use bevy::asset::uuid::Uuid;
use bevy::ecs::world::DeferredWorld;
#[cfg(feature = "picking")]
use bevy::picking::pointer::PointerId;

use crate::viewport::ViewportScalingMode;

/// The pixelated camera component. Adding this component makes this camera render
/// to a small viewport before rendering to the window (or whatever its [`RenderTarget`] was set to).
///
/// ## Implementation details
///
/// Upon adding this component to an entity, 2 children will be added to it, a [`ViewportCamera`] and a [`ViewportImage`],
/// this camera's [`RenderTarget`] will be redirected to the [`ViewportImage`]. Depending on the configured [`viewport_size`],
/// the viewport would be some amount smaller than the window size, which creates the pixelated effect. The [`ViewportCamera`]
/// will point at the small image with zoom and scale it up to the size of the whole window.
///
/// The [`ViewportCamera`] and image are on their own separate [render layer](RenderLayers), (configured with [`viewport_layers`])
/// so that they don't interfere with the world. Any entities added to this layer will be rendered at their full resolution instead
/// of being pixelated.
///
/// [`viewport_size`]: Self::viewport_size
/// [`viewport_layers`]: Self::viewport_layers
#[derive(Component)]
#[require(Camera2d, Msaa::Off)]
#[component(on_add, on_remove)]
pub struct PixelCamera {
    /// The size of the viewport.
    pub viewport_size: ViewportScalingMode,
    /// 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 number higher the than the world camera's order.
    pub viewport_order: isize,
    /// The rendering layer the viewport is on.
    ///
    /// Any entities on this layer will be rendered in high resolution instead of pixelated.
    pub viewport_layers: RenderLayers,
    /// Whether camera position smoothing is enabled for this camera.
    pub smoothing: bool,
}
impl PixelCamera {
    /// Creates a new pixel camera with the `viewport_size` of choice and default configuration.
    #[must_use]
    pub fn from_size(viewport_size: ViewportScalingMode) -> Self {
        Self {
            viewport_size,
            ..default()
        }
    }

    fn on_add(world: DeferredWorld, context: HookContext) {
        fn inner(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) -> Result<()> {
            let this = world.get_entity(entity)?;

            let (pixel_camera, camera, render_target) =
                this.get_components::<(&PixelCamera, &Camera, &RenderTarget)>()?;

            let camera_layers = this.get::<RenderLayers>();
            if let Err(e) = validate_layers(camera_layers, &pixel_camera.viewport_layers) {
                error!(
                    r"While validating RenderLayers for PixelCamera {entity}: {e}
    RenderLayers: {camera_layers:?}
    PixelCamera::viewport_layers: {:?}",
                    &pixel_camera.viewport_layers
                );
            }

            if camera.order >= pixel_camera.viewport_order {
                return Err("The camera is configured to render later or at the same time as of the viewport camera. (camera.order >= viewport_camera.order)".into());
            }
            let window_size = match render_target {
                RenderTarget::Window(window_ref) => {
                    let primary = world
                        .try_query_filtered::<Entity, With<PrimaryWindow>>()
                        .expect("all components should be registered into the world")
                        .single(&world);

                    let mut windows = world
                        .try_query_filtered::<&Window, ()>()
                        .expect("all components should be registered into the world");
                    let window = window_ref
                        .normalize(primary.ok())
                        .and_then(|w| windows.get(&world, w.entity()).ok())
                        .ok_or("RenderTarget::Window points to a window that doesn't exist.")?;

                    Vec2::new(window.width(), window.height())
                }
                RenderTarget::None { size } => size.as_vec2(),
                target => Err(format!("Render target {target:?} is not supported."))?,
            };
            let (size, scaling_mode) = pixel_camera
                .viewport_size
                .get_configuration(window_size, pixel_camera.smoothing);

            // This is the texture that will be rendered to.
            let mut viewport_image = Image::new_target_texture(
                size.width,
                size.height,
                TextureFormat::Rgba8UnormSrgb,
                None,
            );
            viewport_image.texture_descriptor.usage |= TextureUsages::COPY_SRC;
            viewport_image.asset_usage = RenderAssetUsages::RENDER_WORLD;

            let render_target = render_target.clone();

            let viewport_image_handle = world
                .get_resource_mut::<Assets<Image>>()
                .ok_or("resource Assets<Image> should exist, bevy_smooth_pixel_camera expects AssetPlugin to be present")?
                .add(viewport_image);
            let pixel_camera = world
                .get::<PixelCamera>(entity)
                .expect("PixelCamera must exist in it's own on_add hook");

            let viewport_camera = (
                Camera {
                    order: pixel_camera.viewport_order,
                    clear_color: pixel_camera.viewport_size.clear_color().unwrap_or_default(),
                    ..default()
                },
                Projection::Orthographic(OrthographicProjection {
                    scaling_mode,
                    ..OrthographicProjection::default_2d()
                }),
                Camera2d,
                render_target,
                pixel_camera.viewport_layers.clone(),
                ViewportCamera,
                #[cfg(feature = "picking")]
                PointerId::Custom(Uuid::new_v4()),
            );
            let viewport_sprite = (
                Sprite::from_image(viewport_image_handle.clone()),
                pixel_camera.viewport_layers.clone(),
                ViewportImage,
                Transform::from_xyz(0.0, 0.0, -10.0),
                #[cfg(feature = "picking")]
                (Pickable::default(), PointerLocation::default()),
            );
            let mut viewport_camera_id = Entity::PLACEHOLDER;
            let mut viewport_sprite_id = Entity::PLACEHOLDER;
            world
                .commands()
                .entity(entity)
                .insert(RenderTarget::from(viewport_image_handle))
                .with_children(|cmd| {
                    viewport_camera_id = cmd.spawn(viewport_camera).id();
                    viewport_sprite_id = cmd.spawn(viewport_sprite).id();
                })
                .insert(ViewportEntities {
                    camera: viewport_camera_id,
                    sprite: viewport_sprite_id,
                });

            Ok(())
        }

        match inner(world, context) {
            Ok(()) => (),
            Err(err) => {
                if cfg!(test) {
                    panic!("PixelCamera::on_add: {:?}", err);
                } else {
                    error!("While deinitializing PixelCamera {}: {err}", context.entity)
                }
            }
        }
    }

    fn on_remove(world: DeferredWorld, context: HookContext) {
        fn inner(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) -> Result<()> {
            let &ViewportEntities {
                camera,
                sprite: image,
            } = world
                .get(entity)
                .ok_or("PixelCameraViewportEntities should exist")?;

            // Swap RenderTarget
            let world_camera_entity = entity;
            world
                .commands()
                .queue(move |world: &mut World| -> Result<()> {
                    let viewport_entities = world
                        .get::<ViewportEntities>(entity)
                        .ok_or("PixelCameraViewportEntities should exist")?;
                    let [mut world_camera, mut viewport_camera] =
                        world.get_entity_mut([world_camera_entity, viewport_entities.camera])?;
                    let world_target = world_camera
                        .get_mut::<RenderTarget>()
                        .ok_or("no RenderTarget")?;
                    let viewport_target = viewport_camera
                        .get_mut::<RenderTarget>()
                        .ok_or("no RenderTarget")?;
                    mem::swap(world_target.into_inner(), viewport_target.into_inner());

                    Ok(())
                });

            world.commands().entity(camera).despawn();
            world.commands().entity(image).despawn();

            world.commands().entity(entity).remove::<ViewportEntities>();

            Ok(())
        }

        match inner(world, context) {
            Ok(()) => (),
            Err(err) => {
                if cfg!(test) {
                    panic!("PixelCamera::on_remove: {err}");
                } else {
                    error!("While deinitializing PixelCamera {}: {err}", context.entity);
                }
            }
        }
    }
}

impl Default for PixelCamera {
    fn default() -> Self {
        Self {
            viewport_size: ViewportScalingMode::default(),
            viewport_order: 1,
            viewport_layers: RenderLayers::layer(1),
            smoothing: true,
        }
    }
}

/// Marker component for a [`PixelCamera`]'s viewport camera.
///
/// Used when excluding the viewport camera when querying for cameras.
#[derive(Component, Debug)]
pub struct ViewportCamera;

/// Marker component for a [`PixelCamera`]'s viewport image.
#[derive(Component, Debug)]
pub struct ViewportImage;

/// Makes this [`PixelCamera`] high resolution, disabling the pixelation effect.
///
/// The camera will have the same scaling, but the pixels will no longer be locked to the grid.
/// This is useful for when you need to quickly check how the game looks when not locked to the grid.
///
/// Note that high resolution sprites (those on the [`PixelCamera::viewport_layers`]) will no longer
/// always be rendered on top of pixel sprites. Make sure your Z values are set correctly.
// TODO: maybe workaround?
#[derive(Component, Debug)]
#[require(PixelCamera)]
#[component(on_add = Self::swap, on_remove = Self::swap)]
pub struct HighResolution;
impl HighResolution {
    fn swap(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
        world
            .commands()
            .queue(move |world: &mut World| -> Result<()> {
                let &ViewportEntities { camera, sprite } = world
                    .get(entity)
                    .ok_or("PixelCameraViewportEntities should exist")?;
                let [mut world_camera, mut viewport_camera] =
                    world.get_entity_mut([entity, camera])?;
                let world_target = world_camera
                    .get_mut::<RenderTarget>()
                    .ok_or("no RenderTarget")?;
                let viewport_target = viewport_camera
                    .get_mut::<RenderTarget>()
                    .ok_or("no RenderTarget")?;
                mem::swap(world_target.into_inner(), viewport_target.into_inner());

                let world_proj = world_camera
                    .get_mut::<Projection>()
                    .ok_or("no Projection")?;
                let viewport_proj = viewport_camera
                    .get_mut::<Projection>()
                    .ok_or("no Projection")?;
                mem::swap(world_proj.into_inner(), viewport_proj.into_inner());

                let viewport_layers = world_camera
                    .get::<PixelCamera>()
                    .expect(
                        "PixelCamera must exist on hook for component that requires PixelCamera",
                    )
                    .viewport_layers
                    .clone();
                if let Some(mut world_layers) = world_camera.get_mut::<RenderLayers>() {
                    if world_layers.intersects(&viewport_layers) {
                        *world_layers = world_layers.symmetric_difference(&viewport_layers);
                    } else {
                        *world_layers = world_layers.union(&viewport_layers);
                    }
                } else {
                    world
                        .commands()
                        .entity(entity)
                        .insert(RenderLayers::default().union(&viewport_layers));
                }
                if world.get::<Disabled>(sprite).is_some() {
                    world.entity_mut(sprite).remove::<Disabled>();
                } else {
                    world.entity_mut(sprite).insert(Disabled);
                }

                Ok(())
            });
    }
}

// Maybe replace with https://github.com/bevyengine/bevy/issues/21086 when added?
// But even then, Translation propagation is important.
#[derive(Component)]
pub(crate) struct ViewportEntities {
    pub(crate) camera: Entity,
    pub(crate) sprite: Entity,
}

fn validate_layers(
    world_layers: Option<&RenderLayers>,
    viewport_layers: &RenderLayers,
) -> Result<(), &'static str> {
    if let Some(world_layers) = world_layers {
        if world_layers.intersects(viewport_layers) {
            Err(
                "The render layers of the world (PixelCamera) intersect with the render layers of the viewport camera.",
            )
        } else {
            Ok(())
        }
    } else if viewport_layers.intersects(&RenderLayers::default()) {
        Err(
            "The render layers of the viewport camera intersect with the default render layer of the world.",
        )
    } else if viewport_layers == &RenderLayers::none() {
        Err("The viewport camera has no render layers and will not be rendered.")
    } else {
        Ok(())
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use std::assert_matches;

    #[test]
    fn test_pixel_camera_hooks_components() {
        let mut app = test_app();
        let world = app.world_mut();

        let target = RenderTarget::None {
            size: UVec2 { x: 84, y: 905 },
        };
        let projection = Projection::Orthographic(OrthographicProjection {
            far: -421.6,
            ..OrthographicProjection::default_3d()
        });
        let mut camera = world.spawn((target, projection, PixelCamera::default()));

        assert_eq!(camera.get::<Children>().unwrap().len(), 2);
        assert!(camera.get::<ViewportEntities>().is_some());
        assert!(!matches!(
            camera.get::<RenderTarget>(),
            Some(RenderTarget::None {
                size: UVec2 { x: 84, y: 905 }
            })
        ));
        assert_matches!(
            camera.get::<Projection>(),
            Some(Projection::Orthographic(OrthographicProjection {
                far: -421.6,
                ..
            }))
        );
        camera.remove::<PixelCamera>();

        assert_matches!(
            camera.get::<RenderTarget>(),
            Some(RenderTarget::None {
                size: UVec2 { x: 84, y: 905 }
            })
        );
        assert_matches!(
            camera.get::<Projection>(),
            Some(Projection::Orthographic(OrthographicProjection {
                far: -421.6,
                ..
            }))
        );
        assert!(camera.get::<PixelCamera>().is_none());
        assert!(camera.get::<ViewportEntities>().is_none());
        assert!(camera.get::<Children>().is_none());
    }

    #[test]
    fn test_pixel_camera_hooks_children() {
        let mut app = test_app();
        let world = app.world_mut();

        let mut camera = world.spawn(children![Name::new("test")]);
        camera.insert(PixelCamera::default());

        assert_eq!(camera.get::<Children>().unwrap().len(), 3);

        camera.remove::<PixelCamera>();

        let children = camera.get::<Children>().unwrap();
        assert_eq!(children.len(), 1);
        let child = children[0];
        assert_eq!(world.get::<Name>(child).unwrap().as_str(), "test");
    }

    #[test]
    fn test_high_resolution() {
        let mut app = test_app();
        let world = app.world_mut();

        let target = RenderTarget::None {
            size: UVec2 { x: 831, y: 124 },
        };
        let projection = Projection::Orthographic(OrthographicProjection {
            far: 54.1e12,
            ..OrthographicProjection::default_3d()
        });
        let mut camera = world.spawn((target, projection, children![Name::new("test")]));

        assert_eq!(camera.get::<Children>().unwrap().len(), 1);

        camera.insert(PixelCamera::default());

        assert_eq!(camera.get::<Children>().unwrap().len(), 3);

        assert!(!matches!(
            camera.get::<RenderTarget>(),
            Some(RenderTarget::None {
                size: UVec2 { x: 831, y: 124 }
            })
        ));
        assert_matches!(
            camera.get::<Projection>(),
            Some(Projection::Orthographic(OrthographicProjection {
                far: 54.1e12,
                ..
            }))
        );

        camera.insert(HighResolution);

        assert_matches!(
            camera.get::<RenderTarget>(),
            Some(RenderTarget::None {
                size: UVec2 { x: 831, y: 124 }
            })
        );
        assert!(!matches!(
            camera.get::<Projection>(),
            Some(Projection::Orthographic(OrthographicProjection {
                far: 54.1e12,
                ..
            }))
        ));
        assert_eq!(
            camera.get::<RenderLayers>(),
            Some(&RenderLayers::default().union(&RenderLayers::layer(1)))
        );

        let sprite = camera.get::<ViewportEntities>().unwrap().sprite;
        let camera_entity = camera.id();
        assert!(world.get::<Disabled>(sprite).is_some());
        let mut camera = world.entity_mut(camera_entity);

        assert_eq!(camera.get::<Children>().unwrap().len(), 3);

        camera.remove::<HighResolution>();

        assert!(!matches!(
            camera.get::<RenderTarget>(),
            Some(RenderTarget::None {
                size: UVec2 { x: 831, y: 124 }
            })
        ));
        assert_matches!(
            camera.get::<Projection>(),
            Some(Projection::Orthographic(OrthographicProjection {
                far: 54.1e12,
                ..
            }))
        );
        assert_eq!(camera.get::<RenderLayers>(), Some(&RenderLayers::default()));
        assert!(world.get::<Disabled>(sprite).is_none());
    }

    fn test_app() -> App {
        let mut app = App::new();

        app.add_plugins((
            TaskPoolPlugin::default(),
            AssetPlugin::default(),
            ImagePlugin::default(),
            WindowPlugin::default(),
        ));

        app
    }
}