From f574c2c547c367aa7627647e372ead261ab553f3 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Sun, 27 Dec 2020 12:19:03 -0700 Subject: [PATCH] Render text in 2D scenes (#1122) Render text in 2D scenes --- Cargo.toml | 4 + crates/bevy_render/src/render_graph/base.rs | 2 +- .../src/render_graph/nodes/pass_node.rs | 2 +- crates/bevy_text/Cargo.toml | 1 + crates/bevy_text/src/lib.rs | 15 +- crates/bevy_text/src/text.rs | 16 ++ crates/bevy_text/src/text2d.rs | 172 ++++++++++++++++++ crates/bevy_ui/src/entity.rs | 5 +- crates/bevy_ui/src/flex/mod.rs | 3 +- crates/bevy_ui/src/lib.rs | 7 +- crates/bevy_ui/src/node.rs | 5 - crates/bevy_ui/src/widget/image.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 99 ++++------ examples/2d/text2d.rs | 40 ++++ examples/ui/text.rs | 5 +- 15 files changed, 288 insertions(+), 90 deletions(-) create mode 100644 crates/bevy_text/src/text.rs create mode 100644 crates/bevy_text/src/text2d.rs create mode 100644 examples/2d/text2d.rs diff --git a/Cargo.toml b/Cargo.toml index 5fe7da2a41..c30eacdf68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_render/src/render_graph/base.rs b/crates/bevy_render/src/render_graph/base.rs index e1630c4e8f..28eced3a71 100644 --- a/crates/bevy_render/src/render_graph/base.rs +++ b/crates/bevy_render/src/render_graph/base.rs @@ -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; diff --git a/crates/bevy_render/src/render_graph/nodes/pass_node.rs b/crates/bevy_render/src/render_graph/nodes/pass_node.rs index 72f0dfb860..c9de46d256 100644 --- a/crates/bevy_render/src/render_graph/nodes/pass_node.rs +++ b/crates/bevy_render/src/render_graph/nodes/pass_node.rs @@ -37,7 +37,7 @@ pub struct PassNode { impl fmt::Debug for PassNode { 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) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 81e93a34a2..205dad2a47 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -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 diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 33a3b3e8cc..12043a9b9c 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -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; @@ -35,6 +39,11 @@ impl Plugin for TextPlugin { app.add_asset::() .add_asset::() .init_asset_loader::() - .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(), + ); } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs new file mode 100644 index 0000000000..1469aa6b50 --- /dev/null +++ b/crates/bevy_text/src/text.rs @@ -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, + pub style: TextStyle, +} + +#[derive(Default, Copy, Clone, Debug)] +pub struct CalculatedSize { + pub size: Size, +} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs new file mode 100644 index 0000000000..c0e6bb4177 --- /dev/null +++ b/crates/bevy_text/src/text2d.rs @@ -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, + meshes: Res>, + mut render_resource_bindings: ResMut, + text_pipeline: Res, + mut query: Query< + ( + Entity, + &mut Draw, + &Visible, + &Text, + &GlobalTransform, + &CalculatedSize, + ), + With, + >, +) { + 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, +} + +/// Updates the TextGlyphs with the new computed glyphs from the layout +pub fn text2d_system( + mut queued_text: Local, + mut textures: ResMut>, + fonts: Res>, + mut texture_atlases: ResMut>, + mut font_atlas_set_storage: ResMut>, + mut text_pipeline: ResMut, + mut text_queries: QuerySet<( + Query>, + 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; +} diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index f4bd26b708..7eef58b4bd 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -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)] diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/flex/mod.rs index 5e514424f2..475ae9a303 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/crates/bevy_ui/src/flex/mod.rs @@ -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}; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 9e62e520a9..5996bba7cd 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -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::*; diff --git a/crates/bevy_ui/src/node.rs b/crates/bevy_ui/src/node.rs index feb2bc89bb..fc9c482441 100644 --- a/crates/bevy_ui/src/node.rs +++ b/crates/bevy_ui/src/node.rs @@ -48,11 +48,6 @@ impl AddAssign for Val { } } -#[derive(Default, Copy, Clone, Debug)] -pub struct CalculatedSize { - pub size: Size, -} - #[derive(Clone, PartialEq, Debug, Reflect)] pub struct Style { pub display: Display, diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 835d7a520b..051a38769f 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -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 { diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c5dd8d389d..392aff801b 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -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, } -#[derive(Debug, Default, Clone)] -pub struct Text { - pub value: String, - pub font: Handle, - 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, - fonts: &Assets, - texture_atlases: &mut Assets, - font_atlas_set_storage: &mut Assets, - 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, diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs new file mode 100644 index 0000000000..33bd0d16c6 --- /dev/null +++ b/examples/2d/text2d.rs @@ -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) { + 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