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::<Outline>()
|
||||||
.register_type::<BoxShadowSamples>()
|
.register_type::<BoxShadowSamples>()
|
||||||
.register_type::<UiAntiAlias>()
|
.register_type::<UiAntiAlias>()
|
||||||
|
.register_type::<TextShadow>()
|
||||||
.configure_sets(
|
.configure_sets(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
(
|
(
|
||||||
|
@ -10,7 +10,7 @@ mod debug_overlay;
|
|||||||
use crate::widget::ImageNode;
|
use crate::widget::ImageNode;
|
||||||
use crate::{
|
use crate::{
|
||||||
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera,
|
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera,
|
||||||
Outline, ResolvedBorderRadius, UiAntiAlias, UiTargetCamera,
|
Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, UiTargetCamera,
|
||||||
};
|
};
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||||
@ -105,6 +105,7 @@ pub enum RenderUiSystem {
|
|||||||
ExtractImages,
|
ExtractImages,
|
||||||
ExtractTextureSlice,
|
ExtractTextureSlice,
|
||||||
ExtractBorders,
|
ExtractBorders,
|
||||||
|
ExtractTextShadows,
|
||||||
ExtractText,
|
ExtractText,
|
||||||
ExtractDebug,
|
ExtractDebug,
|
||||||
}
|
}
|
||||||
@ -134,6 +135,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||||||
RenderUiSystem::ExtractImages,
|
RenderUiSystem::ExtractImages,
|
||||||
RenderUiSystem::ExtractTextureSlice,
|
RenderUiSystem::ExtractTextureSlice,
|
||||||
RenderUiSystem::ExtractBorders,
|
RenderUiSystem::ExtractBorders,
|
||||||
|
RenderUiSystem::ExtractTextShadows,
|
||||||
RenderUiSystem::ExtractText,
|
RenderUiSystem::ExtractText,
|
||||||
RenderUiSystem::ExtractDebug,
|
RenderUiSystem::ExtractDebug,
|
||||||
)
|
)
|
||||||
@ -146,6 +148,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||||||
extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds),
|
extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds),
|
||||||
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
||||||
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
||||||
|
extract_text_shadows.in_set(RenderUiSystem::ExtractTextShadows),
|
||||||
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
||||||
#[cfg(feature = "bevy_ui_debug")]
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug),
|
debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug),
|
||||||
@ -714,8 +717,8 @@ pub fn extract_text_sections(
|
|||||||
text_styles: Extract<Query<&TextColor>>,
|
text_styles: Extract<Query<&TextColor>>,
|
||||||
camera_map: Extract<UiCameraMap>,
|
camera_map: Extract<UiCameraMap>,
|
||||||
) {
|
) {
|
||||||
let mut start = 0;
|
let mut start = extracted_uinodes.glyphs.len();
|
||||||
let mut end = 1;
|
let mut end = start + 1;
|
||||||
|
|
||||||
let mut camera_mapper = camera_map.get_mapper();
|
let mut camera_mapper = camera_map.get_mapper();
|
||||||
for (
|
for (
|
||||||
@ -743,6 +746,7 @@ pub fn extract_text_sections(
|
|||||||
|
|
||||||
let mut color = LinearRgba::WHITE;
|
let mut color = LinearRgba::WHITE;
|
||||||
let mut current_span = usize::MAX;
|
let mut current_span = usize::MAX;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
i,
|
i,
|
||||||
PositionedGlyph {
|
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)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||||
struct UiVertex {
|
struct UiVertex {
|
||||||
|
@ -2762,3 +2762,23 @@ impl Default for BoxShadowSamples {
|
|||||||
Self(4)
|
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()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
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,
|
font_size: 67.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
TextShadow::default(),
|
||||||
// Set the justification of the Text
|
// Set the justification of the Text
|
||||||
TextLayout::new_with_justify(JustifyText::Center),
|
TextLayout::new_with_justify(JustifyText::Center),
|
||||||
// Set the style of the Node itself.
|
// Set the style of the Node itself.
|
||||||
|
Loading…
Reference in New Issue
Block a user