Skip to content
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

Open
3 of 9 tasks
emilk opened this issue Nov 30, 2020 · 18 comments
Open
3 of 9 tasks

Improve text #56

emilk opened this issue Nov 30, 2020 · 18 comments
Labels
feature New feature or request text Problems related to text Tracking issue

Comments

@emilk
Copy link
Owner

emilk commented Nov 30, 2020

Tracking issue for improved text rendering

A few links (thanks @parasyte):

@emilk emilk added the feature New feature or request label Nov 30, 2020
@msklywenn
Copy link

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)
https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf

It also allows for all sorts of effects (outline, shadows...)

@emilk
Copy link
Owner Author

emilk commented Jan 3, 2021

@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").

@msklywenn
Copy link

msklywenn commented Jan 3, 2021

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.

@emilk
Copy link
Owner Author

emilk commented Jan 3, 2021

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:

  • Quickly generating SDF:s from TTF:s
  • Adding new vertex attributes to switch shader mode
  • More complicated shader

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!

@msklywenn
Copy link

There's no urgent need. I just thought it would fit nicely into that todo list up there :)

@emilk emilk added the text Problems related to text label Jan 16, 2021
@manokara
Copy link

manokara commented Jan 18, 2021

The major benefit would include being able to have dynamic text size instead of picking from a small set of sizes.

Meanwhile, there could be a way to specify a specific font and size in text widgets instead of limiting it to TextStyles. Perhaps doing something like this:

// 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 FontFamily::Custom would be a key into the font_data map. The String in TextStyle::Custom could even be a &'static str for simplicity. Having the liberty of using different font styles for different sections of the UI is necessary for my use case.

@KentaTheBugMaker
Copy link
Contributor

Platform

GPU. Intel hd graphics 620 Mesa 20.3.4
OS. Fedora 33
egui-platform. egui_wgpu_backend 0.4
I override font by under code but japanese text is blank.
I checked font and rusttype but there is no problem.
Is there any platform dependent problem in text rendering?

    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;

@KentaTheBugMaker
Copy link
Contributor

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)
we may need to introduce 3d texture to texture atlas

@KentaTheBugMaker
Copy link
Contributor

rusttype image sample use layout and i tried generate glyph atlas for some short japanese text in my code below
"日本語" by notoSansCJKjp-Light.otf font
glyph.pixel_bounding_box() returns None

    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] });
                //
                }

        }
    }

@KentaTheBugMaker
Copy link
Contributor

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);
}

@KentaTheBugMaker
Copy link
Contributor

I tested WenQuanYiMicroHei with unmodded epaint works fine so i recommend WenQuanYiMicroHei font include

@kirawi
Copy link

kirawi commented Sep 5, 2021

I'll link swash, though tests are still a WIP.

@makoConstruct
Copy link

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 🧓 ⛩️

@kirawi
Copy link

kirawi commented Oct 4, 2021

I think swash supports that as well, but I'm not entirely sure.

@coderedart
Copy link
Contributor

pop-os is doing some stuff which might of interest to this issue. https://github.com/pop-os/cosmic-text

@NatanFreeman
Copy link

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?

@lopo12123
Copy link
Contributor

Maybe this should be included as part of your category "All" of Unicode.

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.

@woelper
Copy link

woelper commented Oct 19, 2023

Maybe this should be included as part of your category "All" of Unicode.

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:
https://lib.rs/crates/egui-phosphor
Basically you just add the font and use the correct codepoint - you can have a look at how egui-phosphor does it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request text Problems related to text Tracking issue
Projects
None yet
Development

No branches or pull requests

10 participants