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" name = "contributors"
path = "examples/2d/contributors.rs" path = "examples/2d/contributors.rs"
[[example]]
name = "text2d"
path = "examples/2d/text2d.rs"
[[example]] [[example]]
name = "load_gltf" name = "load_gltf"
path = "examples/3d/load_gltf.rs" path = "examples/3d/load_gltf.rs"

View file

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

View file

@ -37,7 +37,7 @@ pub struct PassNode<Q: WorldQuery> {
impl<Q: WorldQuery> fmt::Debug for PassNode<Q> { impl<Q: WorldQuery> fmt::Debug for PassNode<Q> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("PassNose") f.debug_struct("PassNode")
.field("descriptor", &self.descriptor) .field("descriptor", &self.descriptor)
.field("inputs", &self.inputs) .field("inputs", &self.inputs)
.field("cameras", &self.cameras) .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_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"] }
bevy_render = { path = "../bevy_render", version = "0.4.0" } bevy_render = { path = "../bevy_render", version = "0.4.0" }
bevy_sprite = { path = "../bevy_sprite", 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" } bevy_utils = { path = "../bevy_utils", version = "0.4.0" }
# other # other

View file

@ -6,6 +6,8 @@ mod font_atlas_set;
mod font_loader; mod font_loader;
mod glyph_brush; mod glyph_brush;
mod pipeline; mod pipeline;
mod text;
mod text2d;
pub use draw::*; pub use draw::*;
pub use error::*; pub use error::*;
@ -15,15 +17,17 @@ pub use font_atlas_set::*;
pub use font_loader::*; pub use font_loader::*;
pub use glyph_brush::*; pub use glyph_brush::*;
pub use pipeline::*; pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub mod prelude { 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}; pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign};
} }
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::AddAsset; use bevy_asset::AddAsset;
use bevy_ecs::Entity; use bevy_ecs::{Entity, IntoSystem};
pub type DefaultTextPipeline = TextPipeline<Entity>; pub type DefaultTextPipeline = TextPipeline<Entity>;
@ -35,6 +39,11 @@ impl Plugin for TextPlugin {
app.add_asset::<Font>() app.add_asset::<Font>()
.add_asset::<FontAtlasSet>() .add_asset::<FontAtlasSet>()
.init_asset_loader::<FontLoader>() .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 super::Node;
use crate::{ use crate::{
render::UI_PIPELINE_HANDLE, render::UI_PIPELINE_HANDLE,
widget::{Button, Image, Text}, widget::{Button, Image},
CalculatedSize, FocusPolicy, Interaction, Style, FocusPolicy, Interaction, Style,
}; };
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::Bundle; use bevy_ecs::Bundle;
@ -15,6 +15,7 @@ use bevy_render::{
prelude::Visible, prelude::Visible,
}; };
use bevy_sprite::{ColorMaterial, QUAD_HANDLE}; use bevy_sprite::{ColorMaterial, QUAD_HANDLE};
use bevy_text::{CalculatedSize, Text};
use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_transform::prelude::{GlobalTransform, Transform};
#[derive(Bundle, Clone, Debug)] #[derive(Bundle, Clone, Debug)]

View file

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

View file

@ -16,12 +16,7 @@ pub use node::*;
pub use render::*; pub use render::*;
pub mod prelude { pub mod prelude {
pub use crate::{ pub use crate::{entity::*, node::*, widget::Button, Anchors, Interaction, Margins};
entity::*,
node::*,
widget::{Button, Text},
Anchors, Interaction, Margins,
};
} }
use bevy_app::prelude::*; 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)] #[derive(Clone, PartialEq, Debug, Reflect)]
pub struct Style { pub struct Style {
pub display: Display, pub display: Display,

View file

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

View file

@ -1,5 +1,5 @@
use crate::{CalculatedSize, Node, Style, Val}; use crate::{Node, Style, Val};
use bevy_asset::{Assets, Handle}; use bevy_asset::Assets;
use bevy_ecs::{Changed, Entity, Local, Or, Query, QuerySet, Res, ResMut}; use bevy_ecs::{Changed, Entity, Local, Or, Query, QuerySet, Res, ResMut};
use bevy_math::Size; use bevy_math::Size;
use bevy_render::{ use bevy_render::{
@ -10,7 +10,9 @@ use bevy_render::{
texture::Texture, texture::Texture,
}; };
use bevy_sprite::{TextureAtlas, QUAD_HANDLE}; 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; use bevy_transform::prelude::GlobalTransform;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -18,13 +20,6 @@ pub struct QueuedText {
entities: Vec<Entity>, 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 /// Defines how min_size, size, and max_size affects the bounds of a text
/// block. /// block.
pub fn text_constraint(min_size: Val, size: Val, max_size: Val) -> f32 { 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(); let query = text_queries.q1_mut();
for entity in queued_text.entities.drain(..) { for entity in queued_text.entities.drain(..) {
if let Ok((text, style, mut calculated_size)) = query.get_mut(entity) { 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, entity,
&*text, text.font.clone(),
&*style, &fonts,
&mut *textures, &text.value,
&*fonts, text.style.font_size,
&mut *texture_atlases, text.style.alignment,
node_size,
&mut *font_atlas_set_storage, &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( let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
"Failed to get glyphs from the pipeline that have just been computed", "Failed to get glyphs from the pipeline that have just been computed",
); );
calculated_size.size = text_layout_info.size; 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; 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)] #[allow(clippy::too_many_arguments)]
pub fn draw_text_system( pub fn draw_text_system(
mut context: DrawContext, 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::*, 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() { fn main() {
App::build() App::build()
.add_plugins(DefaultPlugins) .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>) { fn setup(commands: &mut Commands, asset_server: Res<AssetServer>) {
commands commands
// 2d camera // UI camera
.spawn(CameraUiBundle::default()) .spawn(CameraUiBundle::default())
// texture // texture
.spawn(TextBundle { .spawn(TextBundle {