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

Add frustum gizmo #10038

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
168 changes: 168 additions & 0 deletions crates/bevy_gizmos/src/frustum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Module for the drawing of [`Frustum`]s.

use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{Assets, Handle};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Changed, Or, With, Without},
reflect::ReflectComponent,
removal_detection::RemovedComponents,
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut},
};
use bevy_math::Vec3;
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectFromReflect};
use bevy_render::{color::Color, primitives::Frustum, view::VisibilitySystems};

use crate::{color_from_entity, GizmoConfig, LineGizmo};

/// Plugin for the drawing of [`Frustum`]s.
pub struct FrustumGizmoPlugin;

impl Plugin for FrustumGizmoPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
(
frustum_gizmos,
all_frustum_gizmos.run_if(|config: Res<GizmoConfig>| config.frustum.draw_all),
remove_frustum_gizmos.run_if(|config: Res<GizmoConfig>| !config.frustum.draw_all),
)
.after(VisibilitySystems::UpdateOrthographicFrusta)
.after(VisibilitySystems::UpdatePerspectiveFrusta)
.after(VisibilitySystems::UpdateProjectionFrusta),
);
}
}

/// Configuration for drawing the [`Frustum`] component on entities.
#[derive(Clone, Default)]
pub struct FrustumGizmoConfig {
/// Draws all frusta in the scene when set to `true`.
///
/// To draw a specific entity's frustum, you can add the [`FrustumGizmo`] component.
///
/// Defaults to `false`.
pub draw_all: bool,
/// The default color for frustum gizmos.
///
/// A random color is chosen per frustum if `None`.
///
/// Defaults to `None`.
pub default_color: Option<Color>,
}

/// Add this [`Component`] to an entity to draw its [`Frustum`] component.
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component, FromReflect, Default)]
pub struct FrustumGizmo {
/// The color of the frustum.
///
/// The default color from the [`GizmoConfig`] resource is used if `None`,
pub color: Option<Color>,
}

fn frustum_gizmos(
query: Query<
(Entity, &Frustum, &FrustumGizmo, Option<&Handle<LineGizmo>>),
Or<(Changed<Frustum>, Changed<FrustumGizmo>)>,
>,
config: Res<GizmoConfig>,
mut commands: Commands,
mut lines: ResMut<Assets<LineGizmo>>,
mut removed: RemovedComponents<FrustumGizmo>,
) {
for entity in removed.read() {
if !query.contains(entity) {
commands.entity(entity).remove::<Handle<LineGizmo>>();
}
}

for (entity, frustum, gizmo, line_handle) in &query {
let color = gizmo
.color
.or(config.frustum.default_color)
.unwrap_or_else(|| color_from_entity(entity));

frustum_inner(
&mut commands,
&mut lines,
entity,
frustum,
line_handle,
color,
);
}
}

fn all_frustum_gizmos(
query: Query<
(Entity, &Frustum, Option<&Handle<LineGizmo>>),
(Without<FrustumGizmo>, Changed<Frustum>),
>,
config: Res<GizmoConfig>,
mut commands: Commands,
mut lines: ResMut<Assets<LineGizmo>>,
) {
for (entity, frustum, line_handle) in &query {
let color = config
.frustum
.default_color
.unwrap_or_else(|| color_from_entity(entity));

frustum_inner(
&mut commands,
&mut lines,
entity,
frustum,
line_handle,
color,
);
}
}

fn frustum_inner(
commands: &mut Commands,
lines: &mut ResMut<Assets<LineGizmo>>,
entity: Entity,
frustum: &Frustum,
line_handle: Option<&Handle<LineGizmo>>,
color: Color,
) {
let Some([tln, trn, brn, bln, tlf, trf, brf, blf]) = frustum.corners() else {
return;
};

#[rustfmt::skip]
let positions: Vec<_> = [
tln, trn, brn, bln, tln, // Near
tlf, trf, brf, blf, tlf, // Far
Vec3::NAN, trn, trf, // Near to far
Vec3::NAN, brn, brf,
Vec3::NAN, bln, blf,
].into_iter().map(|v| v.to_array()).collect();

let line = LineGizmo {
colors: std::iter::repeat(color.as_linear_rgba_f32())
.take(positions.len())
.collect(),
positions,
strip: true,
};

if let Some(handle) = line_handle {
lines.insert(handle, line);
} else {
commands.entity(entity).insert(lines.add(line));
}
}

fn remove_frustum_gizmos(
query: Query<Entity, (With<Handle<LineGizmo>>, Without<FrustumGizmo>)>,
mut commands: Commands,
) {
for entity in &query {
commands.entity(entity).remove::<Handle<LineGizmo>>();
}
}
58 changes: 42 additions & 16 deletions crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//!
//! See the documentation on [`Gizmos`] for more examples.

pub mod frustum;
pub mod gizmos;

#[cfg(feature = "bevy_sprite")]
Expand All @@ -26,7 +27,11 @@ mod pipeline_3d;
/// The `bevy_gizmos` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
pub use crate::{
frustum::{FrustumGizmo, FrustumGizmoConfig},
gizmos::Gizmos,
AabbGizmo, AabbGizmoConfig, GizmoConfig,
};
}

use bevy_app::{Last, Plugin, PostUpdate};
Expand Down Expand Up @@ -68,6 +73,8 @@ use bevy_transform::{
use gizmos::{GizmoStorage, Gizmos};
use std::mem;

use crate::frustum::{FrustumGizmoConfig, FrustumGizmoPlugin};

const LINE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7414812689238026784);

/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
Expand All @@ -77,21 +84,24 @@ impl Plugin for GizmoPlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl);

app.add_plugins(UniformComponentPlugin::<LineGizmoUniform>::default())
.init_asset::<LineGizmo>()
.add_plugins(RenderAssetPlugin::<LineGizmo>::default())
.init_resource::<LineGizmoHandles>()
.init_resource::<GizmoConfig>()
.init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes)
.add_systems(
PostUpdate,
(
draw_aabbs,
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
)
.after(TransformSystem::TransformPropagate),
);
app.add_plugins((
FrustumGizmoPlugin,
UniformComponentPlugin::<LineGizmoUniform>::default(),
RenderAssetPlugin::<LineGizmo>::default(),
))
.init_asset::<LineGizmo>()
.init_resource::<LineGizmoHandles>()
.init_resource::<GizmoConfig>()
.init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes)
.add_systems(
PostUpdate,
(
draw_aabbs,
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
)
.after(TransformSystem::TransformPropagate),
);

let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
Expand Down Expand Up @@ -166,6 +176,8 @@ pub struct GizmoConfig {
pub depth_bias: f32,
/// Configuration for the [`AabbGizmo`].
pub aabb: AabbGizmoConfig,
/// Configuration for the [`frustum::FrustumGizmo`].
pub frustum: FrustumGizmoConfig,
/// Describes which rendering layers gizmos will be rendered to.
///
/// Gizmos will only be rendered to cameras with intersecting layers.
Expand All @@ -180,6 +192,7 @@ impl Default for GizmoConfig {
line_perspective: false,
depth_bias: 0.,
aabb: Default::default(),
frustum: Default::default(),
render_layers: Default::default(),
}
}
Expand Down Expand Up @@ -317,6 +330,7 @@ fn extract_gizmo_data(
mut commands: Commands,
handles: Extract<Res<LineGizmoHandles>>,
config: Extract<Res<GizmoConfig>>,
linegizmo_query: Extract<Query<(Entity, &Handle<LineGizmo>)>>,
) {
if config.is_changed() {
commands.insert_resource(config.clone());
Expand All @@ -337,6 +351,18 @@ fn extract_gizmo_data(
handle.clone_weak(),
));
}

for (entity, handle) in &linegizmo_query {
commands.get_or_spawn(entity).insert((
LineGizmoUniform {
line_width: config.line_width,
depth_bias: config.depth_bias,
#[cfg(feature = "webgl")]
_padding: Default::default(),
},
handle.clone_weak(),
));
}
}

#[derive(Component, ShaderType, Clone, Copy)]
Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_gizmos/src/pipeline_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,15 @@ fn queue_line_gizmos_2d(
line_gizmos: Query<(Entity, &Handle<LineGizmo>)>,
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
mut views: Query<(
Entity,
&ExtractedView,
&mut RenderPhase<Transparent2d>,
Option<&RenderLayers>,
)>,
) {
let draw_function = draw_functions.read().get_id::<DrawLineGizmo2d>().unwrap();

for (view, mut transparent_phase, render_layers) in &mut views {
for (view_entity, view, mut transparent_phase, render_layers) in &mut views {
let render_layers = render_layers.copied().unwrap_or_default();
if !config.render_layers.intersects(&render_layers) {
continue;
Expand All @@ -160,6 +161,12 @@ fn queue_line_gizmos_2d(
| Mesh2dPipelineKey::from_hdr(view.hdr);

for (entity, handle) in &line_gizmos {
// The frustum gizmo adds a linegizmo to the same entity.
// We can use that here to prevent views from rendering their own frustum.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice.

if entity == view_entity {
continue;
}

let Some(line_gizmo) = line_gizmo_assets.get(handle) else {
continue;
};
Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_gizmos/src/pipeline_3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,15 @@ fn queue_line_gizmos_3d(
line_gizmos: Query<(Entity, &Handle<LineGizmo>)>,
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
mut views: Query<(
Entity,
&ExtractedView,
&mut RenderPhase<Transparent3d>,
Option<&RenderLayers>,
)>,
) {
let draw_function = draw_functions.read().get_id::<DrawLineGizmo3d>().unwrap();

for (view, mut transparent_phase, render_layers) in &mut views {
for (view_entity, view, mut transparent_phase, render_layers) in &mut views {
let render_layers = render_layers.copied().unwrap_or_default();
if !config.render_layers.intersects(&render_layers) {
continue;
Expand All @@ -173,6 +174,12 @@ fn queue_line_gizmos_3d(
| MeshPipelineKey::from_hdr(view.hdr);

for (entity, handle) in &line_gizmos {
// The frustum gizmo adds a linegizmo to the same entity.
// We can use that here to prevent views from rendering their own frustum.
if entity == view_entity {
continue;
}

let Some(line_gizmo) = line_gizmo_assets.get(handle) else {
continue;
};
Expand Down
40 changes: 40 additions & 0 deletions crates/bevy_render/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,30 @@ impl HalfSpace {
pub fn normal_d(&self) -> Vec4 {
self.normal_d
}

/// Returns the intersection position if the three halfspaces all intersect at a single point.
pub fn intersect(a: HalfSpace, b: HalfSpace, c: HalfSpace) -> Option<Vec3> {
let an = a.normal();
let bn = b.normal();
let cn = c.normal();

let x = Vec3::new(an.x, bn.x, cn.x);
let y = Vec3::new(an.y, bn.y, cn.y);
let z = Vec3::new(an.z, bn.z, cn.z);

let d = -Vec3::new(a.d(), b.d(), c.d());

let u = y.cross(z);
let v = x.cross(d);

let denom = x.dot(u);

if denom.abs() < f32::EPSILON {
return None;
}

Some(Vec3::new(d.dot(u), z.dot(v), -y.dot(v)) / denom)
}
}

/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s.
Expand Down Expand Up @@ -301,6 +325,22 @@ impl Frustum {
}
true
}

/// Calculates the corners of this frustum. Returns `None` if the frustum isn't properly defined.
pub fn corners(&self) -> Option<[Vec3; 8]> {
// TODO Why are these intersection tests failing on spotlight frusta?
let [left, right, top, bottom, near, far] = self.half_spaces;
Some([
HalfSpace::intersect(top, left, near)?,
HalfSpace::intersect(top, right, near)?,
HalfSpace::intersect(bottom, right, near)?,
HalfSpace::intersect(bottom, left, near)?,
HalfSpace::intersect(top, left, far)?,
HalfSpace::intersect(top, right, far)?,
HalfSpace::intersect(bottom, right, far)?,
HalfSpace::intersect(bottom, left, far)?,
])
}
}

#[derive(Component, Debug, Default, Reflect)]
Expand Down
Loading