Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2d frustum culling #3944

Closed
wants to merge 3 commits into from
Closed

Conversation

Weasy666
Copy link
Contributor

@Weasy666 Weasy666 commented Feb 14, 2022

Objective

Enable frustum culling for 2D, to be more precise, for Mesh2d, Sprite and TextureAtlasSprite.

Solution

  • Mesh2d is essentially only a regular Mesh with its handle wrapped by Mesh2dHandle, which enables it to be drawn by the 2d pipeline. As such it already has the compute_aabb() function, which i used for frustum culling the same way it is done for the regular Mesh.
  • Sprite has an image/texture, which provides a size, or a user specified custom_size. I've added ComputedVisibility to the SpriteBundle and use the size to create an Aabb and insert it as a component. The visibility is computed by the already existing check_visibility system.
  • For TextureAtlasSprite i use the user specified custom_size, or a size computed from the sprite's Rect as specified in the TextureAtlas. I've added ComputedVisibility to the SpriteSheetBundle too and the rest is the same as with what i have done for the Sprite.

Benchmarks

System: Windows 10, AMD Ryzen 5 5600XT, AMD Radeon RX 570

Bevymark many_sprites
without culling ~32-33 fps / CPU ~9% ~52 fps / CPU ~19%
with culling ~32-33 fps / CPU ~10% ~60 fps / CPU ~6%

Commands

cargo run --release --example bevymark -- 13 10000
cargo run --release --example many_sprites

Additional stuff

I have run tracy-trace too, here are two captures of many_sprites, one with and one without culling.
tracy_traces.zip

The traces show, that a lot of the additional fps come from the time drop in queue_sprites, from 15.5ms to 43us. That should come from the reduced number of extracted sprites that need to be queued.

@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Feb 14, 2022
@alice-i-cecile alice-i-cecile added C-Performance A change motivated by improving speed, memory usage or compile times A-Rendering Drawing game state to the screen and removed S-Needs-Triage This issue needs to be labelled labels Feb 14, 2022
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The math checks out and the code quality is good. The benchmarks are encouraging too! I really like reducing performance footguns for new users.

However, this needs a lot of tests: this functionality is notoriously fragile. We should test with:

  • scaled sprites
  • scaled cameras
  • orthographic zoomed cameras
  • rotated cameras and sprites
  • sprites that are on the border of the screen
  • sprites that are larger than the camera's view
  • sprites that are moving onto the screen
  • sprites that are just spawned

@@ -178,6 +181,45 @@ impl SpecializedPipeline for SpritePipeline {
}
}

pub fn calculate_bounds(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs doc strings: it's a public system.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ups...that's an oversight, the system doesn't need to be public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm...well maybe not exactly an oversight, because all the other functions in that file are pub too. But i can make it pub(crate) calculate_bounds if you want.

@Weasy666
Copy link
Contributor Author

However, this needs a lot of tests: this functionality is notoriously fragile. We should test with:

  • scaled sprites
  • scaled cameras
  • orthographic zoomed cameras
  • rotated cameras and sprites
  • sprites that are on the border of the screen
  • sprites that are larger than the camera's view
  • sprites that are moving onto the screen
  • sprites that are just spawned

Uff...well...do such tests exist for 3d frustum culling? I am mostly using what is already used for 3d, so it should work equally well for 2d as it does for 3d.

But i have tested all your points

  • scaled sprites: manually tested with sprite and a scale of Vec3::new(2.0, 1.0, 1.0) and Vec3::new(2.0, 2.0, 1.0)
  • scaled cameras: manually tested with sprite and the camera transform set to a scale of Vec3::new(0.5, 0.5, 1.0) and Vec3::new(2.0, 2.0, 1.0)
  • orthographic zoomed cameras: that is essentially just a scaled camera.
  • rotated cameras and sprites: covered by bevymark and many_sprites
  • sprites that are on the border of the screen: manually tested with mesh2d and sprite_sheet, by moving the camera to values around x = 700.
  • sprites that are larger than the camera's view: manually tested with sprite and a scale of Vec3::new(10.0, 10.0, 1.0)
  • sprites that are moving onto the screen: covered by bevymark
  • sprites that are just spawned: covered by bevymark

@alice-i-cecile
Copy link
Member

Uff...well...do such tests exist for 3d frustum culling

There aren't, but there should be :p I didn't review that PR though. It should be fine to create these tests for either 2D or 3D.

But i have tested all your points

It's great to hear that these all work, and it makes me much more confident in this PR. But tests aren't just for verifying that the code works at time of creation; they're essential for ensuring that code isn't accidentally broken in the future <3 I want contributors to feel confident when trying to further optimize or refactor this code, so we should add automated tests for this to the code base.

@Weasy666
Copy link
Contributor Author

Sure, i can understand that and think it is useful. But such tests also don't exist for 3d, so i think that is out of scope for this PR and should probably be its own PR which adds such tests for both cases, 2d and 3d.

Copy link
Contributor

@superdump superdump left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of minor suggestions that are probably inconsequential but worth trying anyway, and please try batch insertion as for many entities it should be faster.

let size = sprite.custom_size.unwrap_or_else(|| image.size());
let aabb = Aabb {
center: Vec3::ZERO,
half_extents: size.extend(0.0) * 0.5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe faster?

Suggested change
half_extents: size.extend(0.0) * 0.5,
half_extents: (0.5 * size).extend(0.0),

Copy link
Contributor Author

@Weasy666 Weasy666 Mar 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will try that. Now that you mention this, i noticed that in some other places divisions are used instead of multiplications, but as far as i know or as far as i was taught, multiplications are preferred because they are (in most cases) faster than divisions. Is this something we want to change? Not in this PR, but in a new one.

.unwrap_or_else(|| (rect.min - rect.max).abs());
let aabb = Aabb {
center: Vec3::ZERO,
half_extents: size.extend(0.0) * 0.5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
half_extents: size.extend(0.0) * 0.5,
half_extents: (0.5 * size).extend(0.0),

for (entity, mesh_handle) in without_aabb.iter() {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some(aabb) = mesh.compute_aabb() {
commands.entity(entity).insert(aabb);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch insertion may be faster. Please try it.

@Moxinilian
Copy link
Member

Moxinilian commented Mar 5, 2022

Thank you for this! I tested it in its current state. Unfortunately it broke the rendering of my project. I have made a small reproducing example:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup)
        .run();
}

const SPRITE_WIDTH: f32 = 32.0;
const SPRITE_HEIGHT: f32 = 32.0;

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let texture = asset_server.load("texture.png");
    let mut camera = OrthographicCameraBundle::new_2d();

    camera.transform.scale.z = 1000.0;
    commands.spawn_bundle(camera);

    for i in 0..10 {
        for j in 0..10 {
            let (x, y) = (i as f32 * SPRITE_WIDTH, j as f32 * SPRITE_HEIGHT);

            commands.spawn_bundle(SpriteBundle {
                texture: texture.clone(),
                transform: Transform::from_translation(Vec3::new(x, y, -y)),
                ..Default::default()
            });
        }
    }
}

Expected result (from current master and bevy 0.6) [sorry for the hexagonal tile, that's what I had at hand]:
image

Result with this PR:
image

@Weasy666
Copy link
Contributor Author

Weasy666 commented Mar 9, 2022

Sorry for the late reply! Can you reproduce this bug with cubes and OrthographicCameraBundle::new_3d()?

EDIT: Tried it myself. 3D seems to not have the same problem. That will be fun to debug 🙃.

The 3D code i used

use bevy::prelude::*;

fn main() {
    App::new()
        .insert_resource(Msaa { samples: 4 })
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup)
        .run();
}

const CUBE_WIDTH: f32 = 0.11;
const CUBE_HEIGHT: f32 = 0.11;

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
) {
    let mesh = meshes.add(Mesh::from(shape::Cube { size: 0.1 }));
    let mut camera = OrthographicCameraBundle::new_3d();
    camera.transform.scale.z = 1000.0;
    commands.spawn_bundle(camera);

    for i in 0..10 {
        for j in 0..10 {
            let (x, y) = (i as f32 * CUBE_WIDTH, j as f32 * CUBE_HEIGHT);
            commands.spawn_bundle(PbrBundle {
                mesh: mesh.clone(),
                transform: Transform::from_translation(Vec3::new(x, y, -y)),
                ..default()
            });
        }
    }
}

@Weasy666
Copy link
Contributor Author

Weasy666 commented Mar 9, 2022

Ok...it boils down to how the two orthographic cameras are set up.
2D has the camera at Transform::from_xyz(0.0, 0.0, 1000.0 - 0.1), while the 3D orthographic camera is at Transform::from_xyz(0.0, 0.0, 0.0). That means that 0 on the z-axis is closest to the 2D orthographic camera, so if z is negative in a sprite transform, then that "moves" the sprite essentially behind the camera, and it gets culled. How do other engines behave in such a case? Is this behavior expected/normal for an orthographic camera?

@mockersf mockersf added the S-Adopt-Me The original PR author has no intent to complete this work. Pick me up! label Dec 25, 2022
@IceSentry
Copy link
Contributor

IceSentry commented Dec 25, 2022

if z is negative in a sprite transform, then that "moves" the sprite essentially behind the camera

In the context of bevy we use a reverse-z, so I would expect negative z to show since it should be in front of the camera.

Other than that, I would indeed expect sprites behind the camera to be culled, it doesn't really make sense to have sprites behind the camera.

@Weasy666
Copy link
Contributor Author

Closing in favor of #7885

@Weasy666 Weasy666 closed this Apr 21, 2023
alice-i-cecile pushed a commit that referenced this pull request Apr 24, 2023
#7885)

# Objective

- Add `Aabb` calculation for `Sprite`, `TextureAtlasSprite` and
`Mesh2d`.
- Enable frustum culling for 2D entities since frustum culling requires
a `Aabb` component in the entity to function.
- Improve 2D performance massively when there are many sprites out of
view. (ex: `many_sprites`)

## Solution

- Derived from @Weasy666's #3944 pull request, which had no activity
since multiple months.
- Adapted the code to the latest version of Bevy.
- Added support for sprites with non-center `Anchor`s to avoid culling
prematurely when part of the sprite is still in view or not culling when
sprite is already out of view.

### Note
- Gives 15.8x performance boosts in some scenarios. (5 fps vs 79 fps
with 409600 sprites in `many_sprites`)

---------

Co-authored-by: ira <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times S-Adopt-Me The original PR author has no intent to complete this work. Pick me up!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants