From a949867a1c3efed8a3465da292a92c59235bbf37 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 1 Jul 2025 20:20:07 +0100 Subject: [PATCH] UI z-ordering fix (#19691) # Objective During the migration to required components a lot of things were changed around and somehow the draw order for some UI elements ended up depending on the system ordering in `RenderSystems::Queue`, which can sometimes result in the elements being drawn in the wrong order. Fixes #19674 ## Solution * Added some more `stack_z_offsets` constants and used them to enforce an explicit ordering. * Removed the `stack_index: u32` field from `ExtractedUiNodes` and replaced it with a `z_order: f32` field. These changes should fix all the ordering problems. ## Testing I added a nine-patched bordered node with a navy background color to the slice section of the `testbed_ui` example. The border should always be drawn above the background color. --- crates/bevy_ui/src/render/debug_overlay.rs | 15 ++++---- crates/bevy_ui/src/render/gradient.rs | 14 ++++++-- crates/bevy_ui/src/render/mod.rs | 33 ++++++++++-------- .../src/render/ui_texture_slice_pipeline.rs | 4 +-- examples/testbed/ui.rs | 20 +++++++++++ .../stack_z_offsets_changes.md | 34 +++++++++++++++++++ 6 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 release-content/migration-guides/stack_z_offsets_changes.md diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index c3ada22c2e..4bf9e2dd93 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,8 +1,14 @@ +use super::ExtractedUiItem; +use super::ExtractedUiNode; +use super::ExtractedUiNodes; +use super::NodeType; +use super::UiCameraMap; use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; +use crate::UiStack; use bevy_asset::AssetId; use bevy_color::Hsla; use bevy_ecs::entity::Entity; @@ -18,12 +24,6 @@ use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use super::ExtractedUiItem; -use super::ExtractedUiNode; -use super::ExtractedUiNodes; -use super::NodeType; -use super::UiCameraMap; - /// Configuration for the UI debug overlay #[derive(Resource)] pub struct UiDebugOptions { @@ -68,6 +68,7 @@ pub fn extract_debug_overlay( &ComputedNodeTarget, )>, >, + ui_stack: Extract>, camera_map: Extract, ) { if !debug_options.enabled { @@ -89,7 +90,7 @@ pub fn extract_debug_overlay( extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), // Add a large number to the UI node's stack index so that the overlay is always drawn on top - stack_index: uinode.stack_index + u32::MAX / 2, + z_order: (ui_stack.uinodes.len() as u32 + uinode.stack_index()) as f32, color: Hsla::sequential_dispersed(entity.index()).into(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index e1c845d481..57541a2ba9 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -408,7 +408,11 @@ pub fn extract_gradients( if let Some(color) = gradient.get_single() { // With a single color stop there's no gradient, fill the node with the color extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: uinode.stack_index, + z_order: uinode.stack_index as f32 + + match node_type { + NodeType::Rect => stack_z_offsets::GRADIENT, + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, color: color.into(), rect: Rect { min: Vec2::ZERO, @@ -629,7 +633,13 @@ pub fn queue_gradient( draw_function, pipeline, entity: (gradient.render_entity, gradient.main_entity), - sort_key: FloatOrd(gradient.stack_index as f32 + stack_z_offsets::GRADIENT), + sort_key: FloatOrd( + gradient.stack_index as f32 + + match gradient.node_type { + NodeType::Rect => stack_z_offsets::GRADIENT, + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, + ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index b3d0117801..dfff318f46 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -81,7 +81,7 @@ pub mod graph { } } -/// Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" +/// Local Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" /// for a given source entity (ex: render both a background color _and_ a custom material for a given node). /// /// When possible these offsets should be defined in _this_ module to ensure z-index coordination across contexts. @@ -97,10 +97,13 @@ pub mod graph { /// a positive offset on a node below. pub mod stack_z_offsets { pub const BOX_SHADOW: f32 = -0.1; - pub const TEXTURE_SLICE: f32 = 0.0; - pub const NODE: f32 = 0.0; - pub const GRADIENT: f32 = 0.1; - pub const MATERIAL: f32 = 0.18267; + pub const BACKGROUND_COLOR: f32 = 0.0; + pub const BORDER: f32 = 0.01; + pub const GRADIENT: f32 = 0.02; + pub const BORDER_GRADIENT: f32 = 0.03; + pub const IMAGE: f32 = 0.04; + pub const MATERIAL: f32 = 0.05; + pub const TEXT: f32 = 0.06; } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] @@ -213,7 +216,7 @@ fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { } pub struct ExtractedUiNode { - pub stack_index: u32, + pub z_order: f32, pub color: LinearRgba, pub rect: Rect, pub image: AssetId, @@ -374,7 +377,7 @@ pub fn extract_uinode_background_colors( extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, + z_order: uinode.stack_index as f32 + stack_z_offsets::BACKGROUND_COLOR, color: background_color.0.into(), rect: Rect { min: Vec2::ZERO, @@ -460,8 +463,8 @@ pub fn extract_uinode_images( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: image.color.into(), rect, clip: clip.map(|clip| clip.clip), @@ -558,7 +561,7 @@ pub fn extract_uinode_borders( completed_flags |= border_flags; extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: computed_node.stack_index, + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, color, rect: Rect { max: computed_node.size(), @@ -591,8 +594,8 @@ pub fn extract_uinode_borders( { let outline_size = computed_node.outlined_node_size(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: computed_node.stack_index, color: outline.color.into(), rect: Rect { max: outline_size, @@ -782,8 +785,8 @@ pub fn extract_viewport_nodes( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: LinearRgba::WHITE, rect: Rect { min: Vec2::ZERO, @@ -885,8 +888,8 @@ pub fn extract_text_sections( .map(|text_color| LinearRgba::from(text_color.0)) .unwrap_or_default(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color, image: atlas_info.texture.id(), clip: clip.map(|clip| clip.clip), @@ -966,8 +969,8 @@ pub fn extract_text_shadows( info.span_index != *span_index || info.atlas_info.texture != atlas_info.texture }) { extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: shadow.color.into(), image: atlas_info.texture.id(), clip: clip.map(|clip| clip.clip), @@ -1023,8 +1026,8 @@ pub fn extract_text_background_colors( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, @@ -1167,7 +1170,7 @@ pub fn queue_uinodes( draw_function, pipeline, entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE), + sort_key: FloatOrd(extracted_uinode.z_order), index, // batch_range will be calculated in prepare_uinodes batch_range: 0..0, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 0e232ab1cc..e36d93ab28 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -366,9 +366,7 @@ pub fn queue_ui_slices( draw_function, pipeline, entity: (extracted_slicer.render_entity, extracted_slicer.main_entity), - sort_key: FloatOrd( - extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE, - ), + sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 10e4e8dc8f..a2f6d6a14a 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -484,6 +484,26 @@ mod slice { }, )); } + + parent.spawn(( + ImageNode { + image: asset_server + .load("textures/fantasy_ui_borders/panel-border-010.png"), + image_mode: NodeImageMode::Sliced(TextureSlicer { + border: BorderRect::all(22.0), + center_scale_mode: SliceScaleMode::Stretch, + sides_scale_mode: SliceScaleMode::Stretch, + max_corner_scale: 1.0, + }), + ..Default::default() + }, + Node { + width: Val::Px(100.), + height: Val::Px(100.), + ..default() + }, + BackgroundColor(bevy::color::palettes::css::NAVY.into()), + )); }); } } diff --git a/release-content/migration-guides/stack_z_offsets_changes.md b/release-content/migration-guides/stack_z_offsets_changes.md new file mode 100644 index 0000000000..04187978d7 --- /dev/null +++ b/release-content/migration-guides/stack_z_offsets_changes.md @@ -0,0 +1,34 @@ +--- +title: Fixed UI draw order and `stack_z_offsets` changes +pull_requests: [19691] +--- + +The draw order of some renderable UI elements relative to others wasn't fixed and depended on system ordering. +In particular the ordering of background colors and texture sliced images was sometimes swapped. + +The UI draw order is now fixed. +The new order is (back-to-front): + +1. Box shadows + +2. Node background colors + +3. Node borders + +4. Gradients + +5. Border Gradients + +6. Images (including texture-sliced images) + +7. Materials + +8. Text (including text shadows) + +The values of the `stack_z_offsets` constants have been updated to enforce the new ordering. Other changes: + +* `NODE` is renamed to `BACKGROUND_COLOR` + +* `TEXTURE_SLICE` is removed, use `IMAGE`. + +* New `BORDER`, `BORDER_GRADIENT` and `TEXT` constants.