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

Introduce support for far culling when using PerspectiveProjection #16789

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bevy_core_pipeline/src/core_2d/camera_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl Camera2dBundle {
// we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset
// the camera's translation by far and use a right handed coordinate system
let projection = OrthographicProjection {
far,
far: Some(far),
..OrthographicProjection::default_2d()
};
let transform = Transform::from_xyz(0.0, 0.0, far - 0.1);
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1435,7 +1435,7 @@ fn load_node(
let xmag = orthographic.xmag();
let orthographic_projection = OrthographicProjection {
near: orthographic.znear(),
far: orthographic.zfar(),
far: Some(orthographic.zfar()),
scaling_mode: ScalingMode::FixedHorizontal {
viewport_width: xmag,
},
Expand All @@ -1451,7 +1451,7 @@ fn load_node(
..Default::default()
};
if let Some(zfar) = perspective.zfar() {
perspective_projection.far = zfar;
perspective_projection.far = Some(zfar);
}
if let Some(aspect_ratio) = perspective.aspect_ratio() {
perspective_projection.aspect_ratio = aspect_ratio;
Expand Down
44 changes: 26 additions & 18 deletions crates/bevy_render/src/camera/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub trait CameraProjection {
fn get_clip_from_view(&self) -> Mat4;
fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4;
fn update(&mut self, width: f32, height: f32);
fn far(&self) -> f32;
fn far(&self) -> Option<f32>;
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];

/// Compute camera frustum for camera with given projection and transform.
Expand All @@ -88,12 +88,16 @@ pub trait CameraProjection {
fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum {
let clip_from_world =
self.get_clip_from_view() * camera_transform.compute_matrix().inverse();
Frustum::from_clip_from_world_custom_far(
&clip_from_world,
&camera_transform.translation(),
&camera_transform.back(),
self.far(),
)

match self.far() {
None => Frustum::from_clip_from_world(&clip_from_world),
Some(far) => Frustum::from_clip_from_world_custom_far(
&clip_from_world,
&camera_transform.translation(),
&camera_transform.back(),
far,
),
}
}
}

Expand Down Expand Up @@ -127,7 +131,7 @@ impl CameraProjection for Projection {
}
}

fn far(&self) -> f32 {
fn far(&self) -> Option<f32> {
match self {
Projection::Perspective(projection) => projection.far(),
Projection::Orthographic(projection) => projection.far(),
Expand Down Expand Up @@ -176,8 +180,8 @@ pub struct PerspectiveProjection {
///
/// Objects farther from the camera than this value will not be visible.
///
/// Defaults to a value of `1000.0`.
pub far: f32,
/// Defaults to `None` (far plane is ignored for visibility checks)
pub far: Option<f32>,
}

impl CameraProjection for PerspectiveProjection {
Expand Down Expand Up @@ -232,7 +236,7 @@ impl CameraProjection for PerspectiveProjection {
.ratio();
}

fn far(&self) -> f32 {
fn far(&self) -> Option<f32> {
self.far
}

Expand Down Expand Up @@ -260,7 +264,7 @@ impl Default for PerspectiveProjection {
PerspectiveProjection {
fov: core::f32::consts::PI / 4.0,
near: 0.1,
far: 1000.0,
far: None,
aspect_ratio: 1.0,
}
}
Expand Down Expand Up @@ -354,8 +358,10 @@ pub struct OrthographicProjection {
///
/// Objects further than this will not be rendered.
///
/// Defaults to `1000.0`
pub far: f32,
/// Defaults to [`OrthographicProjection::DEFAULT_FAR_PLANE`].
///
/// ie: `Some(1000.0)`.
pub far: Option<f32>,
/// Specifies the origin of the viewport as a normalized position from 0 to 1, where (0, 0) is the bottom left
/// and (1, 1) is the top right. This determines where the camera's position sits inside the viewport.
///
Expand Down Expand Up @@ -408,7 +414,7 @@ impl CameraProjection for OrthographicProjection {
self.area.max.y,
// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
// This is for interoperability with pipelines using infinite reverse perspective projections.
self.far,
self.far.unwrap_or(Self::DEFAULT_FAR_PLANE),
self.near,
)
}
Expand Down Expand Up @@ -451,7 +457,7 @@ impl CameraProjection for OrthographicProjection {
top_prime,
// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
// This is for interoperability with pipelines using infinite reverse perspective projections.
self.far,
self.far.unwrap_or(Self::DEFAULT_FAR_PLANE),
self.near,
)
}
Expand Down Expand Up @@ -503,7 +509,7 @@ impl CameraProjection for OrthographicProjection {
);
}

fn far(&self) -> f32 {
fn far(&self) -> Option<f32> {
self.far
}

Expand All @@ -530,6 +536,8 @@ impl FromWorld for OrthographicProjection {
}

impl OrthographicProjection {
pub const DEFAULT_FAR_PLANE: f32 = 1000.0;

/// Returns the default orthographic projection for a 2D context.
///
/// The near plane is set to a negative value so that the camera can still
Expand All @@ -549,7 +557,7 @@ impl OrthographicProjection {
OrthographicProjection {
scale: 1.0,
near: 0.0,
far: 1000.0,
far: Some(Self::DEFAULT_FAR_PLANE),
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this should default to None since bevy currently doesn't do far plane culling.

Copy link
Author

Choose a reason for hiding this comment

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

This is the orthographic case, where bevy does currently do far culling. See #16746 (Additional information)

Copy link
Member

Choose a reason for hiding this comment

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

Does None even make sense for ortho? It doesn't have the ability to use infinite z, because depth is linear.

viewport_origin: Vec2::new(0.5, 0.5),
scaling_mode: ScalingMode::WindowSize,
area: Rect::new(-1.0, -1.0, 1.0, 1.0),
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_render/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,15 +542,15 @@ mod tests {
fov: 90.0_f32.to_radians(),
aspect_ratio: 1.0,
near: 1.0,
far: 100.0,
far: Some(100.0),
};
proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0)))
}

fn contains_aabb_test_frustum_with_rotation() -> Frustum {
let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt();
let near = 50.5 - half_extent_world;
let far = near + 2.0 * half_extent_world;
let far = Some(near + 2.0 * half_extent_world);
let fov = 2.0 * ops::atan(half_extent_world / near);
let proj = PerspectiveProjection {
aspect_ratio: 1.0,
Expand Down
26 changes: 21 additions & 5 deletions crates/bevy_render/src/view/visibility/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use bevy_utils::{Parallel, TypeIdMap};
use super::NoCpuCulling;
use crate::sync_world::MainEntity;
use crate::{
camera::{Camera, CameraProjection},
camera::{Camera, CameraProjection, Projection},
mesh::{Mesh, Mesh3d, MeshAabb},
primitives::{Aabb, Frustum, Sphere},
};
Expand Down Expand Up @@ -507,6 +507,7 @@ pub fn check_visibility<QF>(
&mut VisibleEntities,
&Frustum,
Option<&RenderLayers>,
Option<&Projection>,
&Camera,
Has<NoCpuCulling>,
)>,
Expand All @@ -529,8 +530,15 @@ pub fn check_visibility<QF>(
{
let visible_entity_ranges = visible_entity_ranges.as_deref();

for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in
&mut view_query
for (
view,
mut visible_entities,
frustum,
maybe_view_mask,
maybe_projection,
camera,
no_cpu_culling,
) in &mut view_query
{
if !camera.is_active {
continue;
Expand Down Expand Up @@ -575,17 +583,25 @@ pub fn check_visibility<QF>(
// If we have an aabb, do frustum culling
if !no_frustum_culling && !no_cpu_culling {
if let Some(model_aabb) = maybe_model_aabb {
// Only use far culling if we have a projection with some `far` value.
let use_far_culling = maybe_projection.is_some_and(|p| p.far().is_some());

let world_from_local = transform.affine();
let model_sphere = Sphere {
center: world_from_local.transform_point3a(model_aabb.center),
radius: transform.radius_vec3a(model_aabb.half_extents),
};
// Do quick sphere-based frustum culling
if !frustum.intersects_sphere(&model_sphere, false) {
if !frustum.intersects_sphere(&model_sphere, use_far_culling) {
Copy link
Author

@ptsd ptsd Dec 12, 2024

Choose a reason for hiding this comment

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

I thought this would preserve existing behaviour when using the default projections, but maybe I'm misunderstanding as OrthographicProjection previously far culled even though we always passed false here and to intersects_obb?

return;
}
// Do aabb-based frustum culling
if !frustum.intersects_obb(model_aabb, &world_from_local, true, false) {
if !frustum.intersects_obb(
model_aabb,
&world_from_local,
true,
use_far_culling,
) {
return;
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_sprite/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ fn sprite_picking(
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
continue;
};
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
let cursor_ray_len = cam_ortho
.far
.unwrap_or(OrthographicProjection::DEFAULT_FAR_PLANE)
Copy link
Author

Choose a reason for hiding this comment

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

This is definitely wrong and I'm open to suggestions. Would also allow DEFAULT_FAR_PLANE not to be pub.

- cam_ortho.near;
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;

let picks: Vec<(Entity, HitData)> = sorted_sprites
Expand Down
4 changes: 2 additions & 2 deletions examples/math/custom_primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const TRANSFORM_2D: Transform = Transform {
// The projection used for the camera in 2D
const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
near: -1.0,
far: 10.0,
far: Some(10.0),
scale: 1.0,
viewport_origin: Vec2::new(0.5, 0.5),
scaling_mode: ScalingMode::AutoMax {
Expand All @@ -59,7 +59,7 @@ const TRANSFORM_3D: Transform = Transform {
const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
fov: PI / 4.0,
near: 0.1,
far: 1000.0,
far: Some(1000.0),
aspect_ratio: 1.0,
});

Expand Down
6 changes: 5 additions & 1 deletion examples/tools/scene_viewer/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ fn setup_scene_after_load(
let aabb = Aabb::from_min_max(Vec3::from(min), Vec3::from(max));

info!("Spawning a controllable 3D perspective camera");
let min_required_far = size * 10.0;
let mut projection = PerspectiveProjection::default();
projection.far = projection.far.max(size * 10.0);
projection.far = projection
Copy link
Author

Choose a reason for hiding this comment

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

This preserves the existing intention (I think...) but feels odd as the default would be an essentialy infinite far plane anyway?

.far
.map(|far| far.max(min_required_far))
.or(Some(min_required_far));

let walk_speed = size * 3.0;
let camera_controller = CameraController {
Expand Down
Loading