Conversation
|
|
||
| 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. |
superdump
left a comment
There was a problem hiding this comment.
Left some minor comments, and I think the frustum corner calculations could maybe be on the Frustum impl.
Co-authored-by: Robert Swain <robert.swain@gmail.com>
…ion to public methods
|
Thanks for the comments :) I took a look at the frusta on the lights after reading the comments on discord and noticed that spotlights already have a |
|
I'd use this feature if it was in. @irate-devil any blockers on this? |
# Objective This PR aims to implement multiple configs for gizmos as discussed in #9187. ## Solution Configs for the new `GizmoConfigGroup`s are stored in a `GizmoConfigStore` resource and can be accesses using a type based key or iterated over. This type based key doubles as a standardized location where plugin authors can put their own configuration not covered by the standard `GizmoConfig` struct. For example the `AabbGizmoGroup` has a default color and toggle to show all AABBs. New configs can be registered using `app.init_gizmo_group::<T>()` during startup. When requesting the `Gizmos<T>` system parameter the generic type determines which config is used. The config structs are available through the `Gizmos` system parameter allowing for easy access while drawing your gizmos. Internally, resources and systems used for rendering (up to an including the extract system) are generic over the type based key and inserted on registering a new config. ## Alternatives The configs could be stored as components on entities with markers which would make better use of the ECS. I also implemented this approach ([here](https://github.com/jeliag/bevy/tree/gizmo-multiconf-comp)) and believe that the ergonomic benefits of a central config store outweigh the decreased use of the ECS. ## Unsafe Code Implementing system parameter by hand is unsafe but seems to be required to access the config store once and not on every gizmo draw function call. This is critical for performance. ~Is there a better way to do this?~ ## Future Work New gizmos (such as #10038, and ideas from #9400) will require custom configuration structs. Should there be a new custom config for every gizmo type, or should we group them together in a common configuration? (for example `EditorGizmoConfig`, or something more fine-grained) ## Changelog - Added `GizmoConfigStore` resource and `GizmoConfigGroup` trait - Added `init_gizmo_group` to `App` - Added early returns to gizmo drawing increasing performance when gizmos are disabled - Changed `GizmoConfig` and aabb gizmos to use new `GizmoConfigStore` - Changed `Gizmos` system parameter to use type based key to retrieve config - Changed resources and systems used for gizmo rendering to be generic over type based key - Changed examples (3d_gizmos, 2d_gizmos) to showcase new API ## Migration Guide - `GizmoConfig` is no longer a resource and has to be accessed through `GizmoConfigStore` resource. The default config group is `DefaultGizmoGroup`, but consider using your own custom config group if applicable. --------- Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com>
|
It would be great to have this functionality |
|
I would like to adopt this! 🙂 |
|
@inact1v1ty once you have opened the new PR, I'll close this one |
|
@inact1v1ty are you still intending to adopt this PR? |
|
For anyone needing this just to visualize/debug multiple cameras, here's a single file plugin that adds Frustum gizmos: https://gist.github.com/RCoder01/1285fb3c73177b6c6784cb080bbeb051. It's not perfect (If you get really close to another camera's frustum the lines extend in the opposite direciton) but it was good enough for me. |
# Objective - Adopts #10038 by @tim-blackbird - Half of #19468 (no camera gizmo, just frustum) - Does Part 3 of and fixes #13878 ## Solution I stand on the shoulders of giants and have updated #10038 to main, with the following changes: - The frustum gizmos are now immediate gizmos, not retained - The current view’s frustum is drawn around the border of the screen as a color like so (note the green border at the left, bottom, and right edges of the screen). <img width="1286" height="752" alt="Screenshot 2026-01-31 at 9 18 41 PM" src="https://github.com/user-attachments/assets/7ed2b4db-1710-4be1-b6ca-00725d09944f" /> Also thanks to @RCoder01 for their github gist attached to the original PR; my updates basically ended up being the same although code is more or less in its proper place now. ## Testing - I ran some scene_viewer gltf files in the bevy repo (e.g. `cargo run --example scene_viewer --features "free_camera" -- assets/models/cubes/Cubes.glb`). I guess none have multiple cameras though to cycle through though as far as I could tell? But at least this shows you that toggling the frusta shows the faint border around the screen. - I ran the light_gizmos example `cargo run --example light_gizmos`. Only `SpotLight` has gizmos drawn (`PointLight` and `DirectionalLight` have components that wrap `Frustum` but not `Frustum` itself). It’s the yellow gizmo in the following screenshot. Since there are dedicated light gizmos, using a Frustum gizmo on a light seems unnecessary. <img width="1278" height="740" alt="Screenshot 2026-01-31 at 9 36 54 PM" src="https://github.com/user-attachments/assets/6e676bb8-32d4-4d90-9225-ee3a878745a6" /> - Like the original author, I modified the `split_screen` example to see a camera frustum gizmo from one player on another player’s screen. I removed players 3 and 4, added a frustum gizmo for player 2’s camera, and moved player 1’s camera far enough so that you can see the frustum. <img width="1274" height="740" alt="Screenshot 2026-01-31 at 9 09 46 PM" src="https://github.com/user-attachments/assets/fff38469-9d00-46ba-9098-fdf8418b54fa" /> --- ## Showcase <details> <summary>Modified `split_screen` example code</summary> ```rust //! Renders four cameras to the same window to accomplish "split screen". use std::f32::consts::PI; use bevy::{ camera::Viewport, light::CascadeShadowConfigBuilder, prelude::*, window::WindowResized, }; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (set_camera_viewports, button_system)) .run(); } /// set up a simple 3D scene fn setup( mut commands: Commands, asset_server: Res<AssetServer>, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, ) { // plane commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); commands.spawn(SceneRoot( asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), )); // Light commands.spawn(( Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), DirectionalLight { shadow_maps_enabled: true, ..default() }, CascadeShadowConfigBuilder { num_cascades: if cfg!(all( feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu") )) { // Limited to 1 cascade in WebGL 1 } else { 2 }, first_cascade_far_bound: 200.0, maximum_distance: 280.0, ..default() } .build(), )); // Cameras and their dedicated UI for (index, (camera_name, camera_pos)) in [ ("Player 1", Vec3::new(300.0, 300.0, -150.0)), ("Player 2", Vec3::new(150.0, 150., 50.0)), ] .iter() .enumerate() { let camera = commands .spawn(( Camera3d::default(), Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y), Camera { // Renders cameras with different priorities to prevent ambiguities order: index as isize, ..default() }, CameraPosition { pos: index as u32 % 2, }, ShowFrustumGizmo { color: if index == 0 { Some(Color::NONE) } else { None }, }, )) .id(); // Set up UI if index == 0 { commands.spawn(( UiTargetCamera(camera), Node { width: percent(100), height: percent(100), ..default() }, children![ ( Text::new(*camera_name), Node { position_type: PositionType::Absolute, top: px(12), left: px(12), ..default() }, ), buttons_panel(), ], )); } } fn buttons_panel() -> impl Bundle { ( Node { position_type: PositionType::Absolute, width: percent(100), height: percent(100), display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, align_items: AlignItems::Center, padding: UiRect::all(px(20)), ..default() }, children![ rotate_button("<", Direction::Left), rotate_button(">", Direction::Right), ], ) } fn rotate_button(caption: &str, direction: Direction) -> impl Bundle { ( RotateCamera(direction), Button, Node { width: px(40), height: px(40), border: UiRect::all(px(2)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BorderColor::all(Color::WHITE), BackgroundColor(Color::srgb(0.25, 0.25, 0.25)), children![Text::new(caption)], ) } } #[derive(Component)] struct CameraPosition { pos: u32, } #[derive(Component)] struct RotateCamera(Direction); enum Direction { Left, Right, } fn set_camera_viewports( windows: Query<&Window>, mut window_resized_reader: MessageReader<WindowResized>, mut query: Query<(&CameraPosition, &mut Camera)>, ) { // We need to dynamically resize the camera's viewports whenever the window size changes // so then each camera always takes up half the screen. // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup. for window_resized in window_resized_reader.read() { let window = windows.get(window_resized.window).unwrap(); let size = window.physical_size(); for (camera_position, mut camera) in &mut query { camera.viewport = Some(Viewport { physical_position: camera_position.pos * size, physical_size: size, ..default() }); } } } fn button_system( interaction_query: Query< (&Interaction, &ComputedUiTargetCamera, &RotateCamera), (Changed<Interaction>, With<Button>), >, mut camera_query: Query<&mut Transform, With<Camera>>, ) { for (interaction, computed_target, RotateCamera(direction)) in &interaction_query { if let Interaction::Pressed = *interaction { // Since TargetCamera propagates to the children, we can use it to find // which side of the screen the button is on. if let Some(mut camera_transform) = computed_target .get() .and_then(|camera| camera_query.get_mut(camera).ok()) { let angle = match direction { Direction::Left => -0.1, Direction::Right => 0.1, }; camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle)); } } } } ``` </details> --------- Co-authored-by: devil-ira <justthecooldude@gmail.com> Co-authored-by: Robert Swain <robert.swain@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
|
#22762 includes this PR’s changes, so closing! |
Adds the
FrustumGizmowhich works in a similar fashion to the previously addedAabbGizmo.Closes #4082
Preventing cameras from drawing their own frustums made this a lot messier but as a result this is now technically the first retained gizmo q:
Here's a screenshot of a modified split_screen example showing the frustum of one view in the other.

Changelog
Added the
FrustumGizmocomponent for drawing the frustums of cameras with the relevant settings added to theGizmoConfigresource.