bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.75k stars 3.62k forks source link

Improve and clarify creation of custom CameraProjection #12885

Closed Zoomulator closed 7 months ago

Zoomulator commented 8 months ago

What problem does this solve or what need does it fill?

I've been tinkering with making my own CameraProjection for some less conventional isometric projections. I'm writing this to gather some thoughts on how I've experienced it and what I think could be improved, since I think it's a rarely used feature.

It took me a few hours to figure out how to create my own CameraProjection component. I discovered that there's a CameraProjectionPlugin, but it won't setup the frustum culling resulting in nothing being rendered.

I copied the code that registers the built in projections with update_frusta from here:

https://github.com/bevyengine/bevy/blob/a9964f442d5eac72be90e3810a68225e5da9e622/crates/bevy_render/src/view/visibility/mod.rs#L219-L222

update_frusta calls CameraProjection::compute_frustum() and figures out the culling, which finally gave me a render! Downside is that you have to pull in a bunch of Sets and define system orderings that really feels like it should be more internal to bevy.

update_frusta doesn't touch the get_frustum_corners though. It's curious that get_frustum_corners is required in the trait impl, unlike compute_frustum that has a default implementation. It'd be nice if the corners could be derived from compute_frustum as a default impl of get_frustum_corners too.

The required CameraProjection::far method is called by CameraProjection::compute_frustum, which is convenient in some cases, but it's not necessarily used if you do your own implementation of compute_frustum, forcing you to implement a method that isn't used anywhere. Not all too terrible just returning a dummy f32, but still. Given how easy it is to use one of the Frustum constructors it may even make more sense to remove the default impl of compute_frustum along with far.

The following is the code required to setup your own custom projection:

use bevy::{
    prelude::*,
    render::{
        camera::{camera_system, CameraProjection, CameraProjectionPlugin},
        view::{update_frusta, VisibilitySystems},
    },
    transform::TransformSystem,
};

fn plugin(app: &mut App) {
  app.add_plugins(CameraProjectionPlugin::<MyCameraProjection>::default())
  // The following is adopted from bevy source. Would be nice if this was handled in the CameraProjectionPlugin.
    .add_systems(Update, update_frusta::<MyCameraProjection>
                    .in_set(VisibilitySystems::UpdateProjectionFrusta)
                    .after(camera_system::<MyCameraProjection>)
                    .after(TransformSystem::TransformPropagate))
    .add_systems(Startup, spawn_camera);
}

#[derive(Component, Reflect)]
#[reflect(Component, Default)]
pub struct MyCameraProjection {
    far: f32
}

impl Default for MyCameraProjection {
    fn default() -> Self {
        Self {
            far: 1000.0
        }
    }
}

impl CameraProjection for MyCameraProjection {
    fn get_projection_matrix(&self) -> Mat4 {
        println!("get_projection_matrix");
        // Just a simple orthographic projection.
        Mat4 {
            x_axis: Vec4::new(0.15, 0.0, 0.0, 0.0),
            y_axis: Vec4::new(0.0, 0.3, 0.0, 0.0),
            z_axis: Vec4::new(0.0, 0.0, 0.001, 0.0),
            w_axis: Vec4::new(0.0, 0.0, 1.0, 1.0),
        }
    }

    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
        /* Never called for my project. */
        unimplemented!()
    }

    fn update(&mut self, width: f32, height: f32) {
        println!("update");
    }

   /// Used by the default impl of `CameraProjection::compute_frustum`
    fn far(&self) -> f32 {
        println!("far");
        self.far
    }
}

fn spawn_camera(mut commands: Commands) {
    let mut cam = commands.spawn(Camera3dBundle {
        transform: Transform::from_xyz(10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    });
    // Replace the built in projection.
    cam.remove::<Projection>()
        .insert(MyCameraProjection::default());
}

What solution would you like?

I think the CameraProjectionPlugin could register the update_frusta system for your custom projection, which would be nice. Alternatively, another plugin like UpdateFrustaPlugin::<T: CameraProjection> could be created that fills in those details for you.

What alternative(s) have you considered?

As the example above states, I duplicated code from the bevy source to make it work.

Zoomulator commented 8 months ago

Also worth mentioning that the docs for CameraProjection doesn't cover the requirement of registering the CameraProjectionPlugin and update_frusta to make it work. https://docs.rs/bevy/latest/bevy/render/camera/trait.CameraProjection.html

Components implementing this trait are automatically polled for changes, and used to recompute the camera projection matrix of the Camera component attached to the same entity as the component implementing this trait.

Zoomulator commented 7 months ago

It appears this PR https://github.com/bevyengine/bevy/pull/11808 fixes the issues with update_frusta along with a panic when preparing lights when using camera custom projections.

The PR also adds PbrProjectionPlugin::<T: CameraProjection> which probably should be mentioned in the CameraProjection docs as well? Relevant if you need shadows, and it's where the CameraProjection::get_frustum_corners is used.

Zoomulator commented 7 months ago

https://github.com/bevyengine/bevy/pull/11808 was merged so update_frusta is handled by CameraProjectionPlugin.

Zoomulator commented 7 months ago

https://github.com/bevyengine/bevy/pull/13140 updated the docs so that the plugins are mentioned. The idiosyncrasies of the trait methods are probably better left to its own discussion, so I'll close this now.