From c5c596ae884c5fc34c22fec6cab230cc1df9dffe Mon Sep 17 00:00:00 2001 From: charlotte Date: Fri, 30 May 2025 17:30:43 -0700 Subject: [PATCH 01/11] Add `YSort`, `ZIndex` and `SortBias` for `Sprite` `Mesh2d`. --- Cargo.toml | 11 + crates/bevy_core_pipeline/src/core_2d/mod.rs | 22 +- crates/bevy_gizmos/src/pipeline_2d.rs | 9 +- crates/bevy_sprite/src/mesh2d/material.rs | 16 +- crates/bevy_sprite/src/mesh2d/mesh.rs | 31 ++- crates/bevy_sprite/src/render/mod.rs | 44 +++- crates/bevy_text/src/text2d.rs | 3 + examples/2d/sprite_sorting.rs | 235 +++++++++++++++++++ examples/shader/shader_defs.rs | 4 - 9 files changed, 352 insertions(+), 23 deletions(-) create mode 100644 examples/2d/sprite_sorting.rs diff --git a/Cargo.toml b/Cargo.toml index 9602ef33fbb58..eb6cf2816dae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -778,6 +778,17 @@ description = "Renders an animated sprite" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_sorting" +path = "examples/2d/sprite_sorting.rs" +doc-scrape-examples = true + +[package.metadata.example.sprite_sorting] +name = "Sprite Sorting" +description = "Demonstrates how to sort sprites" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite_tile" path = "examples/2d/sprite_tile.rs" diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 725ac38ed9762..6058504c8c7b4 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -343,9 +343,25 @@ impl CachedRenderPipelinePhaseItem for AlphaMask2d { } } +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +pub struct Transparent2dSortKey { + z_index: i32, + bias: FloatOrd, +} + +impl Transparent2dSortKey { + pub fn new(z_index: i32, bias: Option) -> Transparent2dSortKey { + Transparent2dSortKey { + z_index, + // nans sort after any valid specified y sort + bias: FloatOrd(bias.unwrap_or(f32::NAN)), + } + } +} + /// Transparent 2D [`SortedPhaseItem`]s. pub struct Transparent2d { - pub sort_key: FloatOrd, + pub sort_key: Transparent2dSortKey, pub entity: (Entity, MainEntity), pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, @@ -395,7 +411,7 @@ impl PhaseItem for Transparent2d { } impl SortedPhaseItem for Transparent2d { - type SortKey = FloatOrd; + type SortKey = Transparent2dSortKey; #[inline] fn sort_key(&self) -> Self::SortKey { @@ -405,7 +421,7 @@ impl SortedPhaseItem for Transparent2d { #[inline] fn sort(items: &mut [Self]) { // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. - radsort::sort_by_key(items, |item| item.sort_key().0); + radsort::sort_by_key(items, |item| (item.sort_key.z_index, item.sort_key.bias.0)); } fn indexed(&self) -> bool { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index a97071249d1a4..90b7aad73b83a 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -6,7 +6,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::{load_embedded_asset, Handle}; -use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; +use bevy_core_pipeline::core_2d::{Transparent2d, Transparent2dSortKey, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, @@ -16,7 +16,6 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_image::BevyDefault as _; -use bevy_math::FloatOrd; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, @@ -343,7 +342,7 @@ fn queue_line_gizmos_2d( entity: (entity, *main_entity), draw_function, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, @@ -365,7 +364,7 @@ fn queue_line_gizmos_2d( entity: (entity, *main_entity), draw_function: draw_function_strip, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, @@ -425,7 +424,7 @@ fn queue_line_joint_gizmos_2d( entity: (entity, *main_entity), draw_function, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 3f76b516cdd3f..2b03dd0802644 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -18,7 +18,6 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; -use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::camera::extract_cameras; @@ -49,6 +48,7 @@ use bevy_utils::Parallel; use core::{hash::Hash, marker::PhantomData}; use derive_more::derive::From; use tracing::error; +use bevy_core_pipeline::core_2d::Transparent2dSortKey; /// Materials are used alongside [`Material2dPlugin`], [`Mesh2d`], and [`MeshMaterial2d`] /// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level @@ -839,7 +839,6 @@ pub fn queue_material2d_meshes( }; mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); - let mesh_z = mesh_instance.transforms.world_from_local.translation.z; // We don't support multidraw yet for 2D meshes, so we use this // custom logic to generate the `BinnedRenderPhaseType` instead of @@ -890,6 +889,17 @@ pub fn queue_material2d_meshes( ); } AlphaMode2d::Blend => { + let mesh_y = mesh_instance.transforms.world_from_local.translation.y; + let mesh_z = mesh_instance.transforms.world_from_local.translation.z; + let z_bias = material_2d.properties.depth_bias; + let sort_bias = mesh_instance.sort_bias; + let bias = if mesh_instance.y_sort { + mesh_y + z_bias + sort_bias.unwrap_or_default() + } else { + mesh_z + z_bias + sort_bias.unwrap_or_default() + }; + + let sort_key = Transparent2dSortKey::new(mesh_instance.z_index, Some(bias)); transparent_phase.add(Transparent2d { entity: (*render_entity, *visible_entity), draw_function: material_2d.properties.draw_function_id, @@ -898,7 +908,7 @@ pub fn queue_material2d_meshes( // lowest sort key and getting closer should increase. As we have // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material_2d.properties.depth_bias), + sort_key, // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a3d9ee3eb23fd..0639381e9a619 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -137,6 +137,26 @@ impl Plugin for Mesh2dRenderPlugin { } } +/// Describes the layer in which the sprite or mesh should be rendered. Higher values are rendered +/// on top of lower values, with a default value of 0. This is useful for controlling the rendering +/// order of sprites and always takes precedence over [`YSort`] or [`SortBias`]. +#[derive(Component, Deref, DerefMut, Default, Debug, Clone)] +pub struct ZIndex(pub i32); + +/// A marker component that enables Y-sorting (depth sorting) for sprites and meshes. +/// +/// When attached to an entity, this component indicates that the entity should be rendered +/// in draw order based on its Y position. Entities with lower Y values (higher on screen) +/// are drawn first, creating a depth illusion where objects lower on the screen appear +/// in front of objects higher on the screen. +#[derive(Component, Default, Debug, Clone)] +pub struct YSort; + +/// An arbitrary bias value that can be applied to the sorting order of sprites and meshes and is +/// applied after the [`ZIndex`] or added to the Y position of the entity if [`YSort`] is enabled. +#[derive(Component, Deref, DerefMut, Default, Debug, Clone)] +pub struct SortBias(pub f32); + #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] pub struct ViewKeyCache(MainEntityHashMap); @@ -228,6 +248,9 @@ pub struct RenderMesh2dInstance { pub material_bind_group_id: Material2dBindGroupId, pub automatic_batching: bool, pub tag: u32, + pub z_index: i32, + pub y_sort: bool, + pub sort_bias: Option, } #[derive(Default, Resource, Deref, DerefMut)] @@ -245,13 +268,16 @@ pub fn extract_mesh2d( &GlobalTransform, &Mesh2d, Option<&MeshTag>, + Option<&ZIndex>, + Has, + Option<&SortBias>, Has, )>, >, ) { render_mesh_instances.clear(); - for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query { + for (entity, view_visibility, transform, handle, tag, z_index, y_sort, sort_bias, no_automatic_batching) in &query { if !view_visibility.get() { continue; } @@ -266,6 +292,9 @@ pub fn extract_mesh2d( material_bind_group_id: Material2dBindGroupId::default(), automatic_batching: !no_automatic_batching, tag: tag.map_or(0, |i| **i), + z_index: z_index.map(|x| **x).unwrap_or(0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }, ); } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 7602addc0b793..9c90dbd3fdcde 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,8 +1,8 @@ -use core::ops::Range; - -use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite}; +use crate::{Anchor, ComputedTextureSlices, ScalingMode, SortBias, Sprite, YSort, ZIndex}; use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, Assets, Handle}; + use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_core_pipeline::core_2d::Transparent2dSortKey; use bevy_core_pipeline::{ core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, tonemapping::{ @@ -17,7 +17,7 @@ use bevy_ecs::{ system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo}; -use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; +use bevy_math::{Affine3A, Quat, Rect, Vec2, Vec4}; use bevy_platform::collections::HashMap; use bevy_render::view::{RenderVisibleEntities, RetainedViewEntity}; use bevy_render::{ @@ -41,6 +41,7 @@ use bevy_render::{ }; use bevy_transform::components::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::ops::Range; use fixedbitset::FixedBitSet; #[derive(Resource)] @@ -343,6 +344,9 @@ pub struct ExtractedSprite { pub flip_x: bool, pub flip_y: bool, pub kind: ExtractedSpriteKind, + pub z_index: i32, + pub y_sort: bool, + pub sort_bias: Option, } pub enum ExtractedSpriteKind { @@ -398,13 +402,26 @@ pub fn extract_sprites( &GlobalTransform, &Anchor, Option<&ComputedTextureSlices>, + Option<&ZIndex>, + Has, + Option<&SortBias>, )>, >, ) { extracted_sprites.sprites.clear(); extracted_slices.slices.clear(); - for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in - sprite_query.iter() + for ( + main_entity, + render_entity, + view_visibility, + sprite, + transform, + anchor, + slices, + z_index, + y_sort, + sort_bias, + ) in sprite_query.iter() { if !view_visibility.get() { continue; @@ -427,6 +444,9 @@ pub fn extract_sprites( kind: ExtractedSpriteKind::Slices { indices: start..end, }, + z_index: z_index.cloned().map_or(0, |z| z.0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }); } else { let atlas_rect = sprite @@ -460,6 +480,9 @@ pub fn extract_sprites( // Pass the custom size custom_size: sprite.custom_size, }, + z_index: z_index.cloned().map_or(0, |z| z.0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }); } } @@ -595,7 +618,14 @@ pub fn queue_sprites( } // These items will be sorted by depth with other phase items - let sort_key = FloatOrd(extracted_sprite.transform.translation().z); + let sort_key = Transparent2dSortKey::new( + extracted_sprite.z_index, + extracted_sprite + .y_sort + .then_some(extracted_sprite.transform.translation().y) + .map(|y| y + extracted_sprite.sort_bias.unwrap_or(0.0)) + .or(extracted_sprite.sort_bias), + ); // Add the item to the render phase transparent_phase.add(Transparent2d { diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5069804df8672..9915a0551d59c 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -238,6 +238,9 @@ pub fn extract_text2d_sprite( kind: bevy_sprite::ExtractedSpriteKind::Slices { indices: start..end, }, + z_index: 0, + y_sort: false, + sort_bias: None, }); start = end; } diff --git a/examples/2d/sprite_sorting.rs b/examples/2d/sprite_sorting.rs new file mode 100644 index 0000000000000..07e9a5348b188 --- /dev/null +++ b/examples/2d/sprite_sorting.rs @@ -0,0 +1,235 @@ +//! Demonstrates sprite rendering order using ZIndex, YSort, and SortBias components. +//! +//! This example shows how different sorting methods interact: +//! - `bevy::sprite::ZIndex(i32)`: Absolute rendering order (higher = on top) +//! - `YSort`: Automatic sorting based on Y position (lower Y = behind) +//! - `SortBias`: Fine-tune sorting without changing actual position + +use bevy::sprite::YSort; +use bevy::{prelude::*, sprite::SortBias}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (move_sprites, update_info_text)) + .run(); +} + +#[derive(Component)] +struct Movable { + label: String, +} + +#[derive(Component)] +struct InfoText; + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); + + commands.spawn(( + Text::new( + "WASD: Move white sprite | Q/E: Adjust sort bias\n\ + Hover over sprites to see their sorting properties", + ), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + InfoText, + )); + + // Left + + commands.spawn(( + Sprite { + color: Color::srgb(0.8, 0.2, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-300.0, 50.0, 0.0)), + bevy::sprite::ZIndex(10), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.2, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-280.0, 30.0, 0.0)), + bevy::sprite::ZIndex(5), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.8, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-260.0, 70.0, 0.0)), + bevy::sprite::ZIndex(15), + )); + + // Center + + for i in 0..3 { + let y = -50.0 + i as f32 * 40.0; + commands.spawn(( + Sprite { + color: Color::srgb(0.9, 0.5, 0.1), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-50.0 + i as f32 * 20.0, y, 0.0)), + YSort, + )); + } + + // Right + + commands.spawn(( + Sprite { + color: Color::srgb(0.6, 0.2, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(200.0, 0.0, 0.0)), + YSort, + SortBias(-20.0), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.8, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(220.0, 10.0, 0.0)), + YSort, + SortBias(20.0), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.8, 0.8, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(240.0, 0.0, 0.0)), + YSort, + )); + + // Moveable sprite + commands.spawn(( + Sprite { + color: Color::WHITE, + custom_size: Some(Vec2::splat(50.0)), + ..default() + }, + Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + YSort, + SortBias(0.0), + Movable { + label: "Movable (YSort + Bias: 0)".to_string(), + }, + )); + + // Background grid + for x in -4..=4 { + for y in -3..=3 { + commands.spawn(( + Sprite { + color: Color::srgba(0.3, 0.3, 0.3, 0.1), + custom_size: Some(Vec2::splat(30.0)), + ..default() + }, + Transform::from_translation(Vec3::new(x as f32 * 100.0, y as f32 * 100.0, 0.0)), + bevy::sprite::ZIndex(-100), // Always behind everything + )); + } + } + + // Labels + + commands.spawn(( + Text::new("ZIndex Only"), + Node { + position_type: PositionType::Absolute, + left: Val::Px(300.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); + + commands.spawn(( + Text::new("YSort Only"), + Node { + position_type: PositionType::Absolute, + left: Val::Px(550.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); + + commands.spawn(( + Text::new("YSort + SortBias"), + Node { + position_type: PositionType::Absolute, + right: Val::Px(300.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); +} + +fn move_sprites( + mut query: Query<(&mut Transform, &mut SortBias, &mut Movable)>, + keyboard: Res>, + time: Res