Render text in 2D scenes (#1122)

Render text in 2D scenes
This commit is contained in:
Nathan Stocks 2020-12-27 12:19:03 -07:00 committed by GitHub
parent c32c78fc66
commit f574c2c547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 288 additions and 90 deletions

View file

@ -102,6 +102,10 @@ path = "examples/2d/texture_atlas.rs"
name = "contributors"
path = "examples/2d/contributors.rs"
[[example]]
name = "text2d"
path = "examples/2d/text2d.rs"
[[example]]
name = "load_gltf"
path = "examples/3d/load_gltf.rs"

View file

@ -14,7 +14,7 @@ use bevy_reflect::{Reflect, ReflectComponent};
use bevy_window::WindowId;
/// A component that indicates that an entity should be drawn in the "main pass"
#[derive(Default, Reflect)]
#[derive(Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct MainPass;

View file

@ -37,7 +37,7 @@ pub struct PassNode<Q: WorldQuery> {
impl<Q: WorldQuery> fmt::Debug for PassNode<Q> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("PassNose")
f.debug_struct("PassNode")
.field("descriptor", &self.descriptor)
.field("inputs", &self.inputs)
.field("cameras", &self.cameras)

View file

@ -22,6 +22,7 @@ bevy_math = { path = "../bevy_math", version = "0.4.0" }
bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"] }
bevy_render = { path = "../bevy_render", version = "0.4.0" }
bevy_sprite = { path = "../bevy_sprite", version = "0.4.0" }
bevy_transform = { path = "../bevy_transform", version = "0.4.0" }
bevy_utils = { path = "../bevy_utils", version = "0.4.0" }
# other

View file

@ -6,6 +6,8 @@ mod font_atlas_set;
mod font_loader;
mod glyph_brush;
mod pipeline;
mod text;
mod text2d;
pub use draw::*;
pub use error::*;
@ -15,15 +17,17 @@ pub use font_atlas_set::*;
pub use font_loader::*;
pub use glyph_brush::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub mod prelude {
pub use crate::{Font, TextAlignment, TextError, TextStyle};
pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextStyle};
pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
}
use bevy_app::prelude::*;
use bevy_asset::AddAsset;
use bevy_ecs::Entity;
use bevy_ecs::{Entity, IntoSystem};
pub type DefaultTextPipeline = TextPipeline<Entity>;
@ -35,6 +39,11 @@ impl Plugin for TextPlugin {
app.add_asset::<Font>()
.add_asset::<FontAtlasSet>()
.init_asset_loader::<FontLoader>()
.add_resource(DefaultTextPipeline::default());
.add_resource(DefaultTextPipeline::default())
.add_system_to_stage(bevy_app::stage::POST_UPDATE, text2d_system.system())
.add_system_to_stage(
bevy_render::stage::DRAW,
text2d::draw_text2d_system.system(),
);
}
}

View file

@ -0,0 +1,16 @@
use bevy_asset::Handle;
use bevy_math::Size;
use crate::{Font, TextStyle};
#[derive(Debug, Default, Clone)]
pub struct Text {
pub value: String,
pub font: Handle<Font>,
pub style: TextStyle,
}
#[derive(Default, Copy, Clone, Debug)]
pub struct CalculatedSize {
pub size: Size,
}

View file

@ -0,0 +1,172 @@
use bevy_asset::Assets;
use bevy_ecs::{Bundle, Changed, Entity, Local, Query, QuerySet, Res, ResMut, With};
use bevy_math::{Size, Vec3};
use bevy_render::{
draw::{DrawContext, Drawable},
mesh::Mesh,
prelude::{Draw, Msaa, Texture, Visible},
render_graph::base::MainPass,
renderer::RenderResourceBindings,
};
use bevy_sprite::{TextureAtlas, QUAD_HANDLE};
use bevy_transform::prelude::{GlobalTransform, Transform};
use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
use crate::{
CalculatedSize, DefaultTextPipeline, DrawableText, Font, FontAtlasSet, Text, TextError,
};
/// The bundle of components needed to draw text in a 2D scene via the Camera2dBundle.
#[derive(Bundle, Clone, Debug)]
pub struct Text2dBundle {
pub draw: Draw,
pub visible: Visible,
pub text: Text,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub main_pass: MainPass,
pub calculated_size: CalculatedSize,
}
impl Default for Text2dBundle {
fn default() -> Self {
Self {
draw: Draw {
..Default::default()
},
visible: Visible {
is_transparent: true,
..Default::default()
},
text: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
main_pass: MainPass {},
calculated_size: CalculatedSize {
size: Size::default(),
},
}
}
}
/// System for drawing text in a 2D scene via the Camera2dBundle. Included in the default
/// `TextPlugin`. Position is determined by the `Transform`'s translation, though scale and rotation
/// are ignored.
pub fn draw_text2d_system(
mut context: DrawContext,
msaa: Res<Msaa>,
meshes: Res<Assets<Mesh>>,
mut render_resource_bindings: ResMut<RenderResourceBindings>,
text_pipeline: Res<DefaultTextPipeline>,
mut query: Query<
(
Entity,
&mut Draw,
&Visible,
&Text,
&GlobalTransform,
&CalculatedSize,
),
With<MainPass>,
>,
) {
let font_quad = meshes.get(&QUAD_HANDLE).unwrap();
let vertex_buffer_descriptor = font_quad.get_vertex_buffer_descriptor();
for (entity, mut draw, visible, text, global_transform, calculated_size) in query.iter_mut() {
if !visible.is_visible {
continue;
}
let (width, height) = (calculated_size.size.width, calculated_size.size.height);
if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) {
let position = global_transform.translation
+ match text.style.alignment.vertical {
VerticalAlign::Top => Vec3::zero(),
VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0),
VerticalAlign::Bottom => Vec3::new(0.0, -height, 0.0),
}
+ match text.style.alignment.horizontal {
HorizontalAlign::Left => Vec3::new(-width, 0.0, 0.0),
HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0),
HorizontalAlign::Right => Vec3::zero(),
};
let mut drawable_text = DrawableText {
render_resource_bindings: &mut render_resource_bindings,
position,
msaa: &msaa,
text_glyphs: &text_glyphs.glyphs,
font_quad_vertex_descriptor: &vertex_buffer_descriptor,
style: &text.style,
};
drawable_text.draw(&mut draw, &mut context).unwrap();
}
}
}
#[derive(Debug, Default)]
pub struct QueuedText2d {
entities: Vec<Entity>,
}
/// Updates the TextGlyphs with the new computed glyphs from the layout
pub fn text2d_system(
mut queued_text: Local<QueuedText2d>,
mut textures: ResMut<Assets<Texture>>,
fonts: Res<Assets<Font>>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<DefaultTextPipeline>,
mut text_queries: QuerySet<(
Query<Entity, Changed<Text>>,
Query<(&Text, &mut CalculatedSize)>,
)>,
) {
// Adds all entities where the text or the style has changed to the local queue
for entity in text_queries.q0_mut().iter_mut() {
queued_text.entities.push(entity);
}
if queued_text.entities.is_empty() {
return;
}
// Computes all text in the local queue
let mut new_queue = Vec::new();
let query = text_queries.q1_mut();
for entity in queued_text.entities.drain(..) {
if let Ok((text, mut calculated_size)) = query.get_mut(entity) {
match text_pipeline.queue_text(
entity,
text.font.clone(),
&fonts,
&text.value,
text.style.font_size,
text.style.alignment,
Size::new(f32::MAX, f32::MAX),
&mut *font_atlas_set_storage,
&mut *texture_atlases,
&mut *textures,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the queue for further processing
new_queue.push(entity);
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}.", e);
}
Ok(()) => {
let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
"Failed to get glyphs from the pipeline that have just been computed",
);
calculated_size.size = text_layout_info.size;
}
}
}
}
queued_text.entities = new_queue;
}

View file

@ -1,8 +1,8 @@
use super::Node;
use crate::{
render::UI_PIPELINE_HANDLE,
widget::{Button, Image, Text},
CalculatedSize, FocusPolicy, Interaction, Style,
widget::{Button, Image},
FocusPolicy, Interaction, Style,
};
use bevy_asset::Handle;
use bevy_ecs::Bundle;
@ -15,6 +15,7 @@ use bevy_render::{
prelude::Visible,
};
use bevy_sprite::{ColorMaterial, QUAD_HANDLE};
use bevy_text::{CalculatedSize, Text};
use bevy_transform::prelude::{GlobalTransform, Transform};
#[derive(Bundle, Clone, Debug)]

View file

@ -1,8 +1,9 @@
mod convert;
use crate::{CalculatedSize, Node, Style};
use crate::{Node, Style};
use bevy_ecs::{Changed, Entity, Query, Res, ResMut, With, Without};
use bevy_math::Vec2;
use bevy_text::CalculatedSize;
use bevy_transform::prelude::{Children, Parent, Transform};
use bevy_utils::HashMap;
use bevy_window::{Window, WindowId, Windows};

View file

@ -16,12 +16,7 @@ pub use node::*;
pub use render::*;
pub mod prelude {
pub use crate::{
entity::*,
node::*,
widget::{Button, Text},
Anchors, Interaction, Margins,
};
pub use crate::{entity::*, node::*, widget::Button, Anchors, Interaction, Margins};
}
use bevy_app::prelude::*;

View file

@ -48,11 +48,6 @@ impl AddAssign<f32> for Val {
}
}
#[derive(Default, Copy, Clone, Debug)]
pub struct CalculatedSize {
pub size: Size,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
pub struct Style {
pub display: Display,

View file

@ -1,9 +1,9 @@
use crate::CalculatedSize;
use bevy_asset::{Assets, Handle};
use bevy_ecs::{Query, Res, With};
use bevy_math::Size;
use bevy_render::texture::Texture;
use bevy_sprite::ColorMaterial;
use bevy_text::CalculatedSize;
#[derive(Debug, Clone)]
pub enum Image {

View file

@ -1,5 +1,5 @@
use crate::{CalculatedSize, Node, Style, Val};
use bevy_asset::{Assets, Handle};
use crate::{Node, Style, Val};
use bevy_asset::Assets;
use bevy_ecs::{Changed, Entity, Local, Or, Query, QuerySet, Res, ResMut};
use bevy_math::Size;
use bevy_render::{
@ -10,7 +10,9 @@ use bevy_render::{
texture::Texture,
};
use bevy_sprite::{TextureAtlas, QUAD_HANDLE};
use bevy_text::{DefaultTextPipeline, DrawableText, Font, FontAtlasSet, TextError, TextStyle};
use bevy_text::{
CalculatedSize, DefaultTextPipeline, DrawableText, Font, FontAtlasSet, Text, TextError,
};
use bevy_transform::prelude::GlobalTransform;
#[derive(Debug, Default)]
@ -18,13 +20,6 @@ pub struct QueuedText {
entities: Vec<Entity>,
}
#[derive(Debug, Default, Clone)]
pub struct Text {
pub value: String,
pub font: Handle<Font>,
pub style: TextStyle,
}
/// Defines how min_size, size, and max_size affects the bounds of a text
/// block.
pub fn text_constraint(min_size: Val, size: Val, max_size: Val) -> f32 {
@ -66,26 +61,40 @@ pub fn text_system(
let query = text_queries.q1_mut();
for entity in queued_text.entities.drain(..) {
if let Ok((text, style, mut calculated_size)) = query.get_mut(entity) {
match add_text_to_pipeline(
let node_size = Size::new(
text_constraint(style.min_size.width, style.size.width, style.max_size.width),
text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
),
);
match text_pipeline.queue_text(
entity,
&*text,
&*style,
&mut *textures,
&*fonts,
&mut *texture_atlases,
text.font.clone(),
&fonts,
&text.value,
text.style.font_size,
text.style.alignment,
node_size,
&mut *font_atlas_set_storage,
&mut *text_pipeline,
&mut *texture_atlases,
&mut *textures,
) {
TextPipelineResult::Ok => {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the queue for further processing
new_queue.push(entity);
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}.", e);
}
Ok(()) => {
let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
"Failed to get glyphs from the pipeline that have just been computed",
);
calculated_size.size = text_layout_info.size;
}
TextPipelineResult::Reschedule => {
// There was an error processing the text layout, let's add this entity to the queue for further processing
new_queue.push(entity);
}
}
}
}
@ -93,52 +102,6 @@ pub fn text_system(
queued_text.entities = new_queue;
}
enum TextPipelineResult {
Ok,
Reschedule,
}
/// Computes the text layout and stores it in the TextPipeline resource.
#[allow(clippy::too_many_arguments)]
fn add_text_to_pipeline(
entity: Entity,
text: &Text,
style: &Style,
textures: &mut Assets<Texture>,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>,
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
text_pipeline: &mut DefaultTextPipeline,
) -> TextPipelineResult {
let node_size = Size::new(
text_constraint(style.min_size.width, style.size.width, style.max_size.width),
text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
),
);
match text_pipeline.queue_text(
entity,
text.font.clone(),
&fonts,
&text.value,
text.style.font_size,
text.style.alignment,
node_size,
font_atlas_set_storage,
texture_atlases,
textures,
) {
Err(TextError::NoSuchFont) => TextPipelineResult::Reschedule,
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}.", e);
}
Ok(()) => TextPipelineResult::Ok,
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_text_system(
mut context: DrawContext,

40
examples/2d/text2d.rs Normal file
View file

@ -0,0 +1,40 @@
use bevy::prelude::*;
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.add_system(animate.system())
.run();
}
fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
commands
// 2d camera
.spawn(Camera2dBundle::default())
.spawn(Text2dBundle {
text: Text {
value: "This text is in the 2D scene.".to_string(),
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
style: TextStyle {
font_size: 60.0,
color: Color::WHITE,
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
},
},
..Default::default()
});
}
fn animate(time: Res<Time>, mut query: Query<&mut Transform, With<Text>>) {
// `Transform.translation` will determine the location of the text.
// `Transform.scale` and `Transform.rotation` do not yet affect text (though you can set the
// size of the text via `Text.style.font_size`)
for mut transform in query.iter_mut() {
transform.translation.x = 100.0 * time.seconds_since_startup().sin() as f32;
transform.translation.y = 100.0 * time.seconds_since_startup().cos() as f32;
}
}

View file

@ -3,7 +3,8 @@ use bevy::{
prelude::*,
};
/// This example illustrates how to create text and update it in a system. It displays the current FPS in the upper left hand corner.
/// This example illustrates how to create UI text and update it in a system. It displays the
/// current FPS in the upper left hand corner. For text within a scene, please see the text2d example.
fn main() {
App::build()
.add_plugins(DefaultPlugins)
@ -28,7 +29,7 @@ fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text,
fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
commands
// 2d camera
// UI camera
.spawn(CameraUiBundle::default())
// texture
.spawn(TextBundle {