initial text rendering

This commit is contained in:
Carter Anderson 2020-05-13 13:09:32 -07:00
parent 73cc20768c
commit c18ecf2a55
12 changed files with 240 additions and 10 deletions

View file

@ -6,7 +6,7 @@ edition = "2018"
[features] [features]
default = ["headless", "wgpu", "winit"] default = ["headless", "wgpu", "winit"]
headless = ["asset", "core", "derive", "diagnostic", "gltf", "input", "pbr", "render", "serialization", "transform", "ui", "window"] headless = ["asset", "core", "derive", "diagnostic", "gltf", "input", "pbr", "render", "serialization", "text", "transform", "ui", "window"]
asset = ["bevy_asset"] asset = ["bevy_asset"]
core = ["bevy_core"] core = ["bevy_core"]
derive = ["bevy_derive"] derive = ["bevy_derive"]
@ -16,6 +16,7 @@ input = ["bevy_input"]
pbr = ["bevy_pbr"] pbr = ["bevy_pbr"]
render = ["bevy_render"] render = ["bevy_render"]
serialization = ["bevy_serialization"] serialization = ["bevy_serialization"]
text = ["bevy_text"]
transform = ["bevy_transform"] transform = ["bevy_transform"]
ui = ["bevy_ui"] ui = ["bevy_ui"]
window = ["bevy_window"] window = ["bevy_window"]
@ -42,6 +43,7 @@ bevy_pbr = { path = "crates/bevy_pbr", optional = true }
bevy_render = { path = "crates/bevy_render", optional = true } bevy_render = { path = "crates/bevy_render", optional = true }
bevy_serialization = { path = "crates/bevy_serialization", optional = true } bevy_serialization = { path = "crates/bevy_serialization", optional = true }
bevy_transform = { path = "crates/bevy_transform", optional = true } bevy_transform = { path = "crates/bevy_transform", optional = true }
bevy_text = { path = "crates/bevy_text", optional = true }
bevy_ui = { path = "crates/bevy_ui", optional = true } bevy_ui = { path = "crates/bevy_ui", optional = true }
bevy_window = { path = "crates/bevy_window", optional = true } bevy_window = { path = "crates/bevy_window", optional = true }
bevy_wgpu = { path = "crates/bevy_wgpu", optional = true } bevy_wgpu = { path = "crates/bevy_wgpu", optional = true }
@ -149,6 +151,10 @@ path = "examples/shader/shader_custom_material.rs"
name = "shader_defs" name = "shader_defs"
path = "examples/shader/shader_defs.rs" path = "examples/shader/shader_defs.rs"
[[example]]
name = "text"
path = "examples/ui/text.rs"
[[example]] [[example]]
name = "ui" name = "ui"
path = "examples/ui/ui.rs" path = "examples/ui/ui.rs"

BIN
assets/fonts/FiraMono-Medium.ttf Executable file

Binary file not shown.

BIN
assets/fonts/FiraSans-Bold.ttf Executable file

Binary file not shown.

View file

@ -4,10 +4,6 @@ pub use handle::*;
use bevy_core::bytes::GetBytes; use bevy_core::bytes::GetBytes;
use std::collections::HashMap; use std::collections::HashMap;
pub trait Asset<D> {
fn load(descriptor: D) -> Self;
}
pub struct AssetStorage<T> { pub struct AssetStorage<T> {
assets: HashMap<HandleId, T>, assets: HashMap<HandleId, T>,
names: HashMap<String, Handle<T>>, names: HashMap<String, Handle<T>>,

View file

@ -1,5 +1,5 @@
use crate::shader::ShaderDefSuffixProvider; use crate::shader::ShaderDefSuffixProvider;
use bevy_asset::{Asset, Handle}; use bevy_asset::Handle;
use std::fs::File; use std::fs::File;
pub const TEXTURE_ASSET_INDEX: usize = 0; pub const TEXTURE_ASSET_INDEX: usize = 0;
@ -19,10 +19,8 @@ impl Texture {
pub fn aspect(&self) -> f32 { pub fn aspect(&self) -> f32 {
self.height as f32 / self.width as f32 self.height as f32 / self.width as f32
} }
}
impl Asset<TextureType> for Texture { pub fn load(descriptor: TextureType) -> Self {
fn load(descriptor: TextureType) -> Self {
let (data, width, height) = match descriptor { let (data, width, height) = match descriptor {
TextureType::Data(data, width, height) => (data.clone(), width, height), TextureType::Data(data, width, height) => (data.clone(), width, height),
TextureType::Png(path) => { TextureType::Png(path) => {

View file

@ -0,0 +1,13 @@
[package]
name = "bevy_text"
version = "0.1.0"
authors = ["Carter Anderson <mcanders1@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy_render = { path = "../bevy_render" }
skribo = "0.1.0"
font-kit = "0.6"
pathfinder_geometry = "0.5"

View file

@ -0,0 +1,27 @@
use crate::render::render_text;
use bevy_render::{texture::Texture, Color};
use font_kit::{error::FontLoadingError, metrics::Metrics};
use skribo::{FontCollection, FontFamily};
use std::sync::Arc;
pub struct Font {
pub collection: FontCollection,
pub metrics: Metrics,
}
impl Font {
pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, FontLoadingError> {
let font = font_kit::font::Font::from_bytes(Arc::new(font_data), 0)?;
let metrics = font.metrics();
let mut collection = FontCollection::new();
collection.add_family(FontFamily::new_from_font(font));
Ok(Font {
collection,
metrics,
})
}
pub fn render_text(&self, text: &str, color: Color, width: usize, height: usize) -> Texture {
render_text(self, text, color, width, height)
}
}

View file

@ -0,0 +1,4 @@
mod render;
mod font;
pub use font::*;

View file

@ -0,0 +1,141 @@
use crate::Font;
use bevy_render::{
texture::{Texture, TextureType},
Color,
};
use font_kit::{
canvas::{Canvas, Format, RasterizationOptions},
hinting::HintingOptions,
};
use pathfinder_geometry::transform2d::Transform2F;
use skribo::{LayoutSession, TextStyle};
use std::ops::Range;
struct TextSurface {
width: usize,
height: usize,
pixels: Vec<u8>,
}
fn composite(a: u8, b: u8) -> u8 {
let y = ((255 - a) as u16) * ((255 - b) as u16);
let y = (y + (y >> 8) + 0x80) >> 8; // fast approx to round(y / 255)
255 - (y as u8)
}
impl TextSurface {
fn new(width: usize, height: usize) -> TextSurface {
let pixels = vec![0; width * height];
TextSurface {
width,
height,
pixels,
}
}
fn paint_from_canvas(&mut self, canvas: &Canvas, x: i32, y: i32) {
let (cw, ch) = (canvas.size.x(), canvas.size.y());
let (w, h) = (self.width as i32, self.height as i32);
let y = y - ch;
let xmin = 0.max(-x);
let xmax = cw.min(w - x);
let ymin = 0.max(-y);
let ymax = ch.min(h - y);
for yy in ymin..(ymax.max(ymin)) {
for xx in xmin..(xmax.max(xmin)) {
let pix = canvas.pixels[(cw * yy + xx) as usize];
let dst_ix = ((y + yy) * w + x + xx) as usize;
self.pixels[dst_ix] = composite(self.pixels[dst_ix], pix);
}
}
}
fn paint_layout_session<S: AsRef<str>>(
&mut self,
layout: &mut LayoutSession<S>,
x: i32,
y: i32,
size: f32,
range: Range<usize>,
) {
for run in layout.iter_substr(range) {
let font = run.font();
for glyph in run.glyphs() {
let glyph_id = glyph.glyph_id;
let glyph_x = (glyph.offset.x() as i32) + x;
let glyph_y = (glyph.offset.y() as i32) + y;
let bounds = font
.font
.raster_bounds(
glyph_id,
size,
Transform2F::default(),
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.unwrap();
if bounds.width() > 0 && bounds.height() > 0 {
let origin_adj = bounds.origin().to_f32();
let neg_origin = -origin_adj;
let mut canvas = Canvas::new(bounds.size(), Format::A8);
font.font
.rasterize_glyph(
&mut canvas,
glyph_id,
size,
Transform2F::from_translation(neg_origin),
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.unwrap();
self.paint_from_canvas(
&canvas,
glyph_x + bounds.origin_x(),
glyph_y - bounds.origin_y(),
);
}
}
}
}
}
pub fn render_text(font: &Font, text: &str, color: Color, width: usize, height: usize) -> Texture {
let mut surface = TextSurface::new(width, height);
let style = TextStyle {
size: height as f32,
};
let offset = style.size * (font.metrics.ascent - font.metrics.cap_height)
/ font.metrics.units_per_em as f32;
let mut layout = LayoutSession::create(&text, &style, &font.collection);
surface.paint_layout_session(
&mut layout,
0,
style.size as i32 - offset as i32,
style.size,
0..text.len(),
);
let color_u8 = [
(color.r * 255.0) as u8,
(color.g * 255.0) as u8,
(color.b * 255.0) as u8,
];
Texture::load(TextureType::Data(
surface
.pixels
.iter()
.map(|p| {
vec![
color_u8[0],
color_u8[1],
color_u8[2],
(color.a * *p as f32) as u8,
]
})
.flatten()
.collect::<Vec<u8>>(),
surface.width,
surface.height,
))
}

41
examples/ui/text.rs Normal file
View file

@ -0,0 +1,41 @@
use bevy::prelude::*;
use std::{fs::File, io::Read};
fn main() {
App::build()
.add_default_plugins()
.add_startup_system(setup)
.run();
}
fn setup(world: &mut World, resources: &mut Resources) {
let mut texture_storage = resources.get_mut::<AssetStorage<Texture>>().unwrap();
let font_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/fonts/FiraSans-Bold.ttf"
);
let mut font_file = File::open(&font_path).unwrap();
let mut buffer = Vec::new();
font_file.read_to_end(&mut buffer).unwrap();
let font = Font::try_from_bytes(buffer).unwrap();
let texture = font.render_text("Hello from Bevy!", Color::rgba(0.9, 0.9, 0.9, 1.0), 500, 60);
let half_width = texture.width as f32 / 2.0;
let half_height = texture.height as f32 / 2.0;
let texture_handle = texture_storage.add(texture);
let mut color_materials = resources.get_mut::<AssetStorage<ColorMaterial>>().unwrap();
world
.build()
// 2d camera
.add_entity(Camera2dEntity::default())
// texture
.add_entity(UiEntity {
node: Node::new(
math::vec2(0.0, 0.0),
Anchors::CENTER,
Margins::new(-half_width, half_width, -half_height, half_height),
),
material: color_materials.add(ColorMaterial::texture(texture_handle)),
..Default::default()
});
}

View file

@ -63,6 +63,8 @@ pub use bevy_input as input;
pub use bevy_pbr as pbr; pub use bevy_pbr as pbr;
#[cfg(feature = "render")] #[cfg(feature = "render")]
pub use bevy_render as render; pub use bevy_render as render;
#[cfg(feature = "text")]
pub use bevy_text as text;
#[cfg(feature = "serialization")] #[cfg(feature = "serialization")]
pub use bevy_serialization as serialization; pub use bevy_serialization as serialization;
#[cfg(feature = "transform")] #[cfg(feature = "transform")]

View file

@ -1,5 +1,5 @@
#[cfg(feature = "asset")] #[cfg(feature = "asset")]
pub use crate::asset::{Asset, AssetStorage, Handle}; pub use crate::asset::{AssetStorage, Handle};
#[cfg(feature = "core")] #[cfg(feature = "core")]
pub use crate::core::{ pub use crate::core::{
time::Time, time::Time,
@ -29,6 +29,8 @@ pub use crate::render::{
texture::{Texture, TextureType}, texture::{Texture, TextureType},
ActiveCamera, ActiveCamera2d, Camera, CameraType, Color, ColorSource, Renderable, ActiveCamera, ActiveCamera2d, Camera, CameraType, Color, ColorSource, Renderable,
}; };
#[cfg(feature = "text")]
pub use crate::text::Font;
#[cfg(feature = "transform")] #[cfg(feature = "transform")]
pub use crate::transform::prelude::*; pub use crate::transform::prelude::*;
#[cfg(feature = "ui")] #[cfg(feature = "ui")]