Skip to content

Commit 452b187

Browse files
authored
Text2d shadows (#20463)
# Objective Add support for shadows to `Text2d`. Fixes #19529 ## Solution * New `Text2dShadow` component. `Text2dShadow` has identical fields to `bevy_ui`'s `TextShadow`. Only the `Default` impl is different; since the y-axis is inverted for `Text2d`, the default y-offset needs to be negative. * `extract_text2d_sprite` now also draws shadow sprites when the `Text2dShadow` component is present. Unlike in the `bevy_ui` implementation, the shadows are extracted in the same system as the glyph sprites to ensure correct ordering. ## Testing Updated the `text2d` example to add some shadows to the text boxes: ``` cargo run --example text2d ``` ## Showcase <img width="1924" height="1127" alt="text-2d-shadows" src="https://github.com/user-attachments/assets/ba3c0ae3-a962-4a62-9cee-ea7e328cd80c" />
1 parent 1f5f739 commit 452b187

File tree

4 files changed

+103
-4
lines changed

4 files changed

+103
-4
lines changed

crates/bevy_text/src/text2d.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use bevy_camera::primitives::Aabb;
99
use bevy_camera::visibility::{
1010
self, NoFrustumCulling, ViewVisibility, Visibility, VisibilityClass,
1111
};
12-
use bevy_color::LinearRgba;
12+
use bevy_color::{Color, LinearRgba};
1313
use bevy_derive::{Deref, DerefMut};
1414
use bevy_ecs::entity::EntityHashSet;
1515
use bevy_ecs::{
@@ -132,6 +132,28 @@ pub type Text2dReader<'w, 's> = TextReader<'w, 's, Text2d>;
132132
/// 2d alias for [`TextWriter`].
133133
pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>;
134134

135+
/// Adds a shadow behind `Text2d` text
136+
///
137+
/// Use `TextShadow` for text drawn with `bevy_ui`
138+
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
139+
#[reflect(Component, Default, Debug, Clone, PartialEq)]
140+
pub struct Text2dShadow {
141+
/// Shadow displacement
142+
/// With a value of zero the shadow will be hidden directly behind the text
143+
pub offset: Vec2,
144+
/// Color of the shadow
145+
pub color: Color,
146+
}
147+
148+
impl Default for Text2dShadow {
149+
fn default() -> Self {
150+
Self {
151+
offset: Vec2::new(4., -4.),
152+
color: Color::BLACK,
153+
}
154+
}
155+
}
156+
135157
/// This system extracts the sprites from the 2D text components and adds them to the
136158
/// "render world".
137159
pub fn extract_text2d_sprite(
@@ -148,6 +170,7 @@ pub fn extract_text2d_sprite(
148170
&TextLayoutInfo,
149171
&TextBounds,
150172
&Anchor,
173+
Option<&Text2dShadow>,
151174
&GlobalTransform,
152175
)>,
153176
>,
@@ -170,6 +193,7 @@ pub fn extract_text2d_sprite(
170193
text_layout_info,
171194
text_bounds,
172195
anchor,
196+
maybe_shadow,
173197
global_transform,
174198
) in text2d_query.iter()
175199
{
@@ -183,9 +207,60 @@ pub fn extract_text2d_sprite(
183207
);
184208

185209
let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size;
210+
211+
if let Some(shadow) = maybe_shadow {
212+
let shadow_transform = *global_transform
213+
* GlobalTransform::from_translation((top_left + shadow.offset).extend(0.))
214+
* scaling;
215+
let color = shadow.color.into();
216+
217+
for (
218+
i,
219+
PositionedGlyph {
220+
position,
221+
atlas_info,
222+
..
223+
},
224+
) in text_layout_info.glyphs.iter().enumerate()
225+
{
226+
let rect = texture_atlases
227+
.get(atlas_info.texture_atlas)
228+
.unwrap()
229+
.textures[atlas_info.location.glyph_index]
230+
.as_rect();
231+
extracted_slices.slices.push(ExtractedSlice {
232+
offset: Vec2::new(position.x, -position.y),
233+
rect,
234+
size: rect.size(),
235+
});
236+
237+
if text_layout_info
238+
.glyphs
239+
.get(i + 1)
240+
.is_none_or(|info| info.atlas_info.texture != atlas_info.texture)
241+
{
242+
let render_entity = commands.spawn(TemporaryRenderEntity).id();
243+
extracted_sprites.sprites.push(ExtractedSprite {
244+
main_entity,
245+
render_entity,
246+
transform: shadow_transform,
247+
color,
248+
image_handle_id: atlas_info.texture,
249+
flip_x: false,
250+
flip_y: false,
251+
kind: bevy_sprite::ExtractedSpriteKind::Slices {
252+
indices: start..end,
253+
},
254+
});
255+
start = end;
256+
}
257+
258+
end += 1;
259+
}
260+
}
261+
186262
let transform =
187263
*global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling;
188-
189264
let mut color = LinearRgba::WHITE;
190265
let mut current_span = usize::MAX;
191266

crates/bevy_ui/src/widget/text.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl From<String> for Text {
130130

131131
/// Adds a shadow behind text
132132
///
133-
/// Not supported by `Text2d`
133+
/// Use the `Text2dShadow` component for `Text2d` shadows
134134
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
135135
#[reflect(Component, Default, Debug, Clone, PartialEq)]
136136
pub struct TextShadow {

examples/2d/text2d.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bevy::{
1010
math::ops,
1111
prelude::*,
1212
sprite::Anchor,
13-
text::{FontSmoothing, LineBreak, TextBounds},
13+
text::{FontSmoothing, LineBreak, Text2dShadow, TextBounds},
1414
};
1515

1616
fn main() {
@@ -47,13 +47,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
4747
Text2d::new("translation"),
4848
text_font.clone(),
4949
TextLayout::new_with_justify(text_justification),
50+
Text2dShadow::default(),
5051
AnimateTranslation,
5152
));
5253
// Demonstrate changing rotation
5354
commands.spawn((
5455
Text2d::new("rotation"),
5556
text_font.clone(),
5657
TextLayout::new_with_justify(text_justification),
58+
Text2dShadow::default(),
5759
AnimateRotation,
5860
));
5961
// Demonstrate changing scale
@@ -62,6 +64,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
6264
text_font,
6365
TextLayout::new_with_justify(text_justification),
6466
Transform::from_translation(Vec3::new(400.0, 0.0, 0.0)),
67+
Text2dShadow::default(),
6568
AnimateScale,
6669
));
6770
// Demonstrate text wrapping
@@ -72,6 +75,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
7275
};
7376
let box_size = Vec2::new(300.0, 200.0);
7477
let box_position = Vec2::new(0.0, -250.0);
78+
let box_color = Color::srgb(0.25, 0.25, 0.55);
79+
let text_shadow_color = box_color.darker(0.05);
7580
commands.spawn((
7681
Sprite::from_color(Color::srgb(0.25, 0.25, 0.55), box_size),
7782
Transform::from_translation(box_position.extend(0.0)),
@@ -83,6 +88,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
8388
TextBounds::from(box_size),
8489
// Ensure the text is drawn on top of the box
8590
Transform::from_translation(Vec3::Z),
91+
// Add a shadow to the text
92+
Text2dShadow {
93+
color: text_shadow_color,
94+
..default()
95+
},
8696
)],
8797
));
8898

@@ -99,6 +109,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
99109
TextBounds::from(other_box_size),
100110
// Ensure the text is drawn on top of the box
101111
Transform::from_translation(Vec3::Z),
112+
// Add a shadow to the text
113+
Text2dShadow {
114+
color: text_shadow_color,
115+
..default()
116+
}
102117
)],
103118
));
104119

@@ -110,6 +125,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
110125
.with_font_smoothing(FontSmoothing::None),
111126
TextLayout::new_with_justify(Justify::Center),
112127
Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)),
128+
// Add a black shadow to the text
129+
Text2dShadow::default(),
113130
));
114131

115132
commands
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: Text2d Shadows
3+
authors: ["@Ickshonpe"]
4+
pull_requests: [20463]
5+
---
6+
7+
`Text2d` now supports shadows. Add the `Text2dShadow` component to a `Text2d` entity to draw a shadow beneath its text.

0 commit comments

Comments
 (0)