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.
This commit is contained in:
ickshonpe 2025-07-01 20:20:07 +01:00 committed by GitHub
parent 5e8aa7986b
commit a949867a1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 93 additions and 27 deletions

View File

@ -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<Res<UiStack>>,
camera_map: Extract<UiCameraMap>,
) {
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,

View File

@ -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,

View File

@ -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<Image>,
@ -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,

View File

@ -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,

View File

@ -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()),
));
});
}
}

View File

@ -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.