-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve text #56
Comments
I think storing the text has a signed distance field instead of a simple white on black bitmap should also be considered. (scales better and uses less memory) It also allows for all sorts of effects (outline, shadows...) |
@msklywenn SDF text has some benefits, like being able to scale text, but come with the downside that the Egui fragment shader will no longer be a simple texture sampler. Currently the same shader supports showing text as well as images. If we switch to a distance field font we would need a more complicated shader taking in both a distance field texture and a normal texture to conveniently support both use cases (or have two shaders and switch between them which comes with its own downsides). I'd like to keep Egui integration as simple as possible for now. I would also dispute that SDF fonts use less memory - that is only really true for when one want to write really large characters (which Egui doesn't really need). For small characters SDF:s actually tend to use more texels (margin for the blurry "glow"). |
Basic SDF rendering only needs alpha testing. To have one shader compatible with bitmap and SDF rendering, a solution would be to have an offset be sent to the shader and applied before discarding. With an offset of 0, discarding would never happen and bitmap display would occur just like it does now. With a -0.5 offset, negative alpha values would be discarded. That offset could then also be negated and added so that the pixels that pass don't look half translucent. In my experience, SDF looks better even at low resolution as it can be thought of a cheap antialiasing. Also, the scaling of SDF resolves the problem of hi-DPI screens, it is not just to render big texts. |
On low-DPI screens alpha-tested text will look horrible, but I take your point of having a single texture sampler and the programmatically controlling whether to treat it as an SDF or as a color image. Still, there is a lot of complexity here:
The major benefit would include being able to have dynamic text size instead of picking from a small set of sizes. Is there an urgent need for this? Otherwise I don't see SDF text as being a high priority. If you feel differently please open a separate issue on it and we can continue the discussion there! |
There's no urgent need. I just thought it would fit nicely into that todo list up there :) |
Meanwhile, there could be a way to specify a specific font and size in text widgets instead of limiting it to // app setup
font_definitions.custom_styles.insert(
"noto-sans-bold-14".to_owned(),
(FontFamily::Custom("noto-sans-bold".to_owned()), 14)
);
// ui update
ui.add(Label::new("foo").text_style(TextStyle::Custom("noto-sans-bold-14".to_owned())); Where the string in |
Platform GPU. Intel hd graphics 620 Mesa 20.3.4 let mut font_def=FontDefinitions::default();
let mut font_data:BTreeMap<String,Cow<'static,[u8]>> =BTreeMap::new();
let mut fonts_for_family:BTreeMap<FontFamily, Vec<String>>=BTreeMap::new();
font_data.insert("NotoSans".to_owned(), Cow::Borrowed(include_bytes!("../NotoSans-Light.ttf")));
font_data.insert("NotoSansCJKjp".to_owned(), Cow::Borrowed(include_bytes!("../NotoSansCJKjp-Light.otf")));
font_def.font_data=font_data;
//font_def.family_and_size.insert(TextStyle::Body,(FontFamily::Proportional,24.0));
fonts_for_family.insert(FontFamily::Monospace,vec![
"NotoSans".to_owned(),
"NotoSansCJKjp".to_owned(),
]);
fonts_for_family.insert(FontFamily::Proportional,vec![
"NotoSans".to_owned(),
"NotoSansCJKjp".to_owned(),
]);
font_def.fonts_for_family=fonts_for_family; |
I squashed 40000+ chars to 3000+ chars NotoSansCJKjp-Light and expand texture atlas to 0x4000 by 0x4000 from original 2048 by 64 but all japanese texts are tofu (white block) and hit some limits in GPU(MAX_TEXTURE_SIZE) |
rusttype image sample use layout and i tried generate glyph atlas for some short japanese text in my code below pub fn add_new_glyph_to_atlas(&mut self,c:GlyphId){
let glyph=self.font.glyph(c).scaled(Scale::uniform(self.scale as f32));
let glyph =glyph.positioned(Point{ x: 0.0, y: 0.0 });
match glyph.pixel_bounding_box(){
None => {
// no glyph
println!("no glyph for GlyphId {}",c.0)
}
Some(bb) => {
//if texture size over allocate new line
println!("origin.x {} max.x {}",self.texture.origin[0],bb.max.x);
if (self.texture.origin[0]+ bb.max.x as usize )>self.texture.dimension[0] {
//add line
self.texture.origin[1]+=self.scale as usize;
//extend texture height
self.texture.dimension[1]+=self.scale as usize;
//allocate new line
self.texture.data.extend_from_slice(&vec![0;self.texture.dimension[0]*self.scale as usize]);
// origin.x to 0
self.texture.origin[0]=0;
println!("alloc new line")
}else {
self.texture.origin[0]+=bb.max.x as usize;
}
glyph.draw(|x, y, v| {
//write data to texture
self.texture.data[
(self.texture.origin[1] + y as usize) * self.texture.dimension[0] +(self.texture.origin[0] + x as usize)
] = (v * 255.0) as u8;
});
//write where the top left
self.font_cache.insert(c,Rect{ position: [self.texture.origin[0],self.texture.origin[1]], size: [self.scale as usize,self.scale as usize] });
//
}
}
} |
I found darty workable font caching for japanese but NotoSansCJKjp-Light.otf is not workable with rusttype use std::collections::HashMap;
use rusttype::{Scale, Point, GlyphId, point, PositionedGlyph};
use image::ImageFormat;
use std::sync::Arc;
pub struct Texture{
dimension:[usize;2],
data:Vec<u8>,
origin:[usize;2],
}
pub struct Font{
font_cache:FontCache,
scale:i32,
texture:Texture,
glyph_per_line:i32,
font:Arc<rusttype::Font<'static>>,
name:&'static str
}
impl Font{
pub fn new_from_bytes(data: &'static[u8],name:&'static str,gpl:i32,scale:i32) ->Self{
let font=rusttype::Font::try_from_bytes(data).expect("Invalid font data");
println!(" glyphs {}", font.glyph_count());
// left one pixel margin for avoid debris
let one_line=( scale+1)*gpl;
Self{ font_cache: Default::default(), scale, texture: Texture { dimension: [one_line as usize,1+scale as usize], data:vec![0;(one_line*(scale+1))as usize], origin: [0,0] }, glyph_per_line: gpl, font:Arc::new(font), name }
}
pub fn add_new_glyph_to_atlas(&mut self,c:char){
// is font_cache contains glyph for c?
// then skip rasterize
if self.font_cache.contains_key(&c){
return
}
let tmp_str=c.to_string();
let fc=self.font.clone();
let glyphs=fc.layout(&tmp_str,Scale::uniform(self.scale as f32),point(0.0,0.0));
for glyph in glyphs{
if let Some(bounding_box)=glyph.pixel_bounding_box(){
//if texture size over allocate new line
if (self.texture.origin[0]+1+self.scale as usize )>self.texture.dimension[0] {
//add line
self.texture.origin[1]+=1+self.scale as usize;
//extend texture height
self.texture.dimension[1]+=1+self.scale as usize;
//allocate new line
self.texture.data.extend_from_slice(&vec![0;self.texture.dimension[0]*(2+self.scale as usize)]);
// origin.x to 1
self.texture.origin[0]=1;
println!("alloc new line")
}else {
self.texture.origin[0]=self.texture.origin[0] +1+ self.scale as usize;
}
glyph.draw(|x, y, v| {
//write data to texture
println!("Debug {}",self.texture.origin[1] );
let address=(self.texture.origin[1] + y as usize) * self.texture.dimension[0] +(self.texture.origin[0] + x as usize);
if address<self.texture.data.len() {
self.texture.data[address] = (v * 255.0) as u8;
}
});
//write where the top left
self.font_cache.insert(c,Rect{ position: [self.texture.origin[0],self.texture.origin[1]], size: [self.scale as usize,self.scale as usize] });
}else{
println!("No glyph for {}",c)
}
}
}
}
#[derive(Copy, Clone,Eq, PartialEq,Ord, PartialOrd)]
pub struct Rect{
position:[usize;2],
size:[usize;2],
}
pub type FontCache=HashMap<char,Rect>;
#[test]
fn text_rusttype_texture_atlas(){
let test_text="日本語でレンダリング!";
let mut font =Font::new_from_bytes(include_bytes!("../WenQuanYiMicroHei.ttf"), "WenQuanyiMicroHei", 10,32);
for ch in test_text.chars(){
font.add_new_glyph_to_atlas(ch)
}
let img =image::GrayImage::from_raw(font.texture.dimension[0] as u32,font.texture.dimension[1] as u32,font.texture.data).expect("Failed to create image");
img.save_with_format("WenQuanYiMicroHei-32.bmp",ImageFormat::Bmp);
} |
I tested WenQuanYiMicroHei with unmodded epaint works fine so i recommend WenQuanYiMicroHei font include |
I'll link swash, though tests are still a WIP. |
You probably also want to look into font hinting, it nudges the edges of the vectors to make text look more crisp on older screens. It used to be standard, for newer higher resolution screens it's not so important. I just wish that the old screens wouldn't be left behind 🧓 ⛩️ |
I think swash supports that as well, but I'm not entirely sure. |
pop-os is doing some stuff which might of interest to this issue. https://github.com/pop-os/cosmic-text |
I can't use this for my purpose without right-to-left character support. Is there a workout I can use until this is added? |
Maybe this should be included as part of your category Is there currently a way to use font icons, such as Material Symbols and iconfont? If possible, it will be very convenient to use some simple plain icons. Reasons to use font icon instead of image: When I use png or svg as the icon, there will be obvious distortion (such as jagged or missing elements) when the size is small. |
Yes! There are even crates for that, for example for Phosphor icons: |
Tracking issue for improved text rendering
TextStyle
(Choose your own font and size #1154)epaint::text::Glyph
should own a grapheme cluster, not a char #2532A few links (thanks @parasyte):
The text was updated successfully, but these errors were encountered: