Basic UI text shadows (#17559)
# Objective Basic `TextShadow` support. ## Solution New `TextShadow` component with `offset` and `color` fields. Just insert it on a `Text` node to add a shadow. New system `extract_text_shadows` handles rendering. It's not "real" shadows just the text redrawn with an offset and a different colour. Blur-radius support will need changes to the shaders and be a lot more complicated, whereas this still looks okay and took a couple of minutes to implement. I added the `TextShadow` component to `bevy_ui` rather than `bevy_text` because it only supports the UI atm. We can add a `Text2d` version in a followup but getting the same effect in `Text2d` is trivial even without official support. --- ## Showcase <img width="122" alt="text_shadow" src="https://github.com/user-attachments/assets/0333d167-c507-4262-b93b-b6d39e2cf3a4" /> <img width="136" alt="g" src="https://github.com/user-attachments/assets/9b01d5d9-55c9-4af7-9360-a7b04f55944d" />
This commit is contained in:
parent
ca6b07c348
commit
03ec6441a7
@ -167,6 +167,7 @@ impl Plugin for UiPlugin {
|
||||
.register_type::<Outline>()
|
||||
.register_type::<BoxShadowSamples>()
|
||||
.register_type::<UiAntiAlias>()
|
||||
.register_type::<TextShadow>()
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
(
|
||||
|
@ -10,7 +10,7 @@ mod debug_overlay;
|
||||
use crate::widget::ImageNode;
|
||||
use crate::{
|
||||
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera,
|
||||
Outline, ResolvedBorderRadius, UiAntiAlias, UiTargetCamera,
|
||||
Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, UiTargetCamera,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||
@ -105,6 +105,7 @@ pub enum RenderUiSystem {
|
||||
ExtractImages,
|
||||
ExtractTextureSlice,
|
||||
ExtractBorders,
|
||||
ExtractTextShadows,
|
||||
ExtractText,
|
||||
ExtractDebug,
|
||||
}
|
||||
@ -134,6 +135,7 @@ pub fn build_ui_render(app: &mut App) {
|
||||
RenderUiSystem::ExtractImages,
|
||||
RenderUiSystem::ExtractTextureSlice,
|
||||
RenderUiSystem::ExtractBorders,
|
||||
RenderUiSystem::ExtractTextShadows,
|
||||
RenderUiSystem::ExtractText,
|
||||
RenderUiSystem::ExtractDebug,
|
||||
)
|
||||
@ -146,6 +148,7 @@ pub fn build_ui_render(app: &mut App) {
|
||||
extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds),
|
||||
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
||||
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
||||
extract_text_shadows.in_set(RenderUiSystem::ExtractTextShadows),
|
||||
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug),
|
||||
@ -714,8 +717,8 @@ pub fn extract_text_sections(
|
||||
text_styles: Extract<Query<&TextColor>>,
|
||||
camera_map: Extract<UiCameraMap>,
|
||||
) {
|
||||
let mut start = 0;
|
||||
let mut end = 1;
|
||||
let mut start = extracted_uinodes.glyphs.len();
|
||||
let mut end = start + 1;
|
||||
|
||||
let mut camera_mapper = camera_map.get_mapper();
|
||||
for (
|
||||
@ -743,6 +746,7 @@ pub fn extract_text_sections(
|
||||
|
||||
let mut color = LinearRgba::WHITE;
|
||||
let mut current_span = usize::MAX;
|
||||
|
||||
for (
|
||||
i,
|
||||
PositionedGlyph {
|
||||
@ -799,6 +803,105 @@ pub fn extract_text_sections(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_text_shadows(
|
||||
mut commands: Commands,
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
&ComputedNode,
|
||||
&GlobalTransform,
|
||||
&InheritedVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&UiTargetCamera>,
|
||||
&TextLayoutInfo,
|
||||
&TextShadow,
|
||||
)>,
|
||||
>,
|
||||
mapping: Extract<Query<RenderEntity>>,
|
||||
) {
|
||||
let mut start = extracted_uinodes.glyphs.len();
|
||||
let mut end = start + 1;
|
||||
|
||||
let default_ui_camera = default_ui_camera.get();
|
||||
for (
|
||||
entity,
|
||||
uinode,
|
||||
global_transform,
|
||||
inherited_visibility,
|
||||
clip,
|
||||
camera,
|
||||
text_layout_info,
|
||||
shadow,
|
||||
) in &uinode_query
|
||||
{
|
||||
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_ui_camera) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`)
|
||||
if !inherited_visibility.get() || uinode.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let transform = global_transform.affine()
|
||||
* Mat4::from_translation(
|
||||
(-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.),
|
||||
);
|
||||
|
||||
let mut current_span = usize::MAX;
|
||||
for (
|
||||
i,
|
||||
PositionedGlyph {
|
||||
position,
|
||||
atlas_info,
|
||||
span_index,
|
||||
..
|
||||
},
|
||||
) in text_layout_info.glyphs.iter().enumerate()
|
||||
{
|
||||
if *span_index != current_span {
|
||||
current_span = *span_index;
|
||||
}
|
||||
|
||||
let rect = texture_atlases
|
||||
.get(&atlas_info.texture_atlas)
|
||||
.unwrap()
|
||||
.textures[atlas_info.location.glyph_index]
|
||||
.as_rect();
|
||||
extracted_uinodes.glyphs.push(ExtractedGlyph {
|
||||
transform: transform * Mat4::from_translation(position.extend(0.)),
|
||||
rect,
|
||||
});
|
||||
|
||||
if text_layout_info.glyphs.get(i + 1).is_none_or(|info| {
|
||||
info.span_index != current_span || info.atlas_info.texture != atlas_info.texture
|
||||
}) {
|
||||
extracted_uinodes.uinodes.push(ExtractedUiNode {
|
||||
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),
|
||||
extracted_camera_entity,
|
||||
rect,
|
||||
item: ExtractedUiItem::Glyphs { range: start..end },
|
||||
main_entity: entity.into(),
|
||||
});
|
||||
start = end;
|
||||
}
|
||||
|
||||
end += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
struct UiVertex {
|
||||
|
@ -2762,3 +2762,23 @@ impl Default for BoxShadowSamples {
|
||||
Self(4)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a shadow behind text
|
||||
#[derive(Component, Copy, Clone, Debug, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
pub struct TextShadow {
|
||||
/// Shadow displacement in logical pixels
|
||||
/// With a value of zero the shadow will be hidden directly behind the text
|
||||
pub offset: Vec2,
|
||||
/// Color of the shadow
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Default for TextShadow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
offset: Vec2::splat(4.),
|
||||
color: Color::linear_rgba(0., 0., 0., 0.75),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
TextShadow::default(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
font_size: 67.0,
|
||||
..default()
|
||||
},
|
||||
TextShadow::default(),
|
||||
// Set the justification of the Text
|
||||
TextLayout::new_with_justify(JustifyText::Center),
|
||||
// Set the style of the Node itself.
|
||||
|
Loading…
Reference in New Issue
Block a user