Nice fonts without bleeding at all sizes using tiled atlas

This commit is contained in:
bloeys
2022-07-03 12:27:16 +04:00
parent a4b7dfd395
commit 9c09814322
3 changed files with 58 additions and 28 deletions

View File

@ -2,6 +2,7 @@ package glyphs
import ( import (
"errors" "errors"
"fmt"
"image" "image"
"image/draw" "image/draw"
"image/png" "image/png"
@ -36,7 +37,9 @@ type FontAtlasGlyph struct {
} }
//NewFontAtlasFromFile reads a TTF or TTC file and produces a font texture atlas containing //NewFontAtlasFromFile reads a TTF or TTC file and produces a font texture atlas containing
//all its characters using the specified options. //all its characters using the specified options. The atlas uses equally sized tiles
//such that all characters use an equal horizontal/vertical on the atlas.
//If the character is smaller than the tile then the rest of the tile is empty.
// //
//Only monospaced fonts are supported //Only monospaced fonts are supported
func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*FontAtlas, error) { func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*FontAtlas, error) {
@ -56,9 +59,11 @@ func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*Font
} }
//NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing //NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing
//all its characters using the specified options. //all its characters using the specified options. The atlas uses equally sized tiles
//such that all characters use an equal horizontal/vertical on the atlas.
//If the character is smaller than the tile then the rest of the tile is empty.
// //
//Only monospaced fonts are supported //Only monospaced fonts are supported.
func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) { func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) {
const maxAtlasSize = 8192 const maxAtlasSize = 8192
@ -66,30 +71,47 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f)) glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f))
assert.T(len(glyphs) > 0, "no glyphs") assert.T(len(glyphs) > 0, "no glyphs")
//Choose atlas size //Find advance and line height
atlasSizeX := 64
atlasSizeY := 64
const charPaddingX = 2 const charPaddingX = 2
const charPaddingY = 2 const charPaddingY = 2
charAdvFixed, _ := face.GlyphAdvance('L') charAdvFixed, _ := face.GlyphAdvance('L')
charAdv := charAdvFixed.Ceil() + charPaddingX charAdv := charAdvFixed.Ceil() + charPaddingX
lineHeight := face.Metrics().Height.Ceil() //Find largest vertical character.
//We don't use face.Metrics().Height because its not reliable
lineHeightFixed := fixed.Int26_6(0)
for _, g := range glyphs {
maxLinesInAtlas := atlasSizeY/lineHeight - 1 gBounds, _, _ := face.GlyphBounds(g)
ascent := absFixedI26_6(gBounds.Min.Y)
descent := absFixedI26_6(gBounds.Max.Y)
charHeight := ascent + descent
if charHeight > lineHeightFixed {
lineHeightFixed = charHeight
}
}
lineHeightFixed = fixed.I(lineHeightFixed.Ceil())
lineHeight := lineHeightFixed.Ceil()
fmt.Println("calculated line height:", lineHeight)
//Calculate needed atlas size
atlasSizeX := 64
atlasSizeY := 64
maxLinesInAtlas := atlasSizeY/lineHeight - 2
charsPerLine := atlasSizeX / charAdv charsPerLine := atlasSizeX / charAdv
linesNeeded := int(math.Ceil(float64(len(glyphs)) / float64(charsPerLine))) linesNeeded := int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1
for linesNeeded > maxLinesInAtlas { for linesNeeded > maxLinesInAtlas {
atlasSizeX *= 2 atlasSizeX *= 2
atlasSizeY *= 2 atlasSizeY *= 2
maxLinesInAtlas = atlasSizeY/lineHeight - 1 maxLinesInAtlas = atlasSizeY/lineHeight - 2
charsPerLine = atlasSizeX / charAdv charsPerLine = atlasSizeX / charAdv
linesNeeded = int(math.Ceil(float64(len(glyphs)) / float64(charsPerLine))) linesNeeded = int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1
} }
if atlasSizeX > maxAtlasSize { if atlasSizeX > maxAtlasSize {
@ -106,7 +128,6 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
//Clear background to black //Clear background to black
draw.Draw(atlas.Img, atlas.Img.Bounds(), image.Black, image.Point{}, draw.Src) draw.Draw(atlas.Img, atlas.Img.Bounds(), image.Black, image.Point{}, draw.Src)
drawer := &font.Drawer{ drawer := &font.Drawer{
Dst: atlas.Img, Dst: atlas.Img,
Src: image.White, Src: image.White,
@ -120,7 +141,6 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
charPaddingYFixed := fixed.I(charPaddingY) charPaddingYFixed := fixed.I(charPaddingY)
charsOnLine := 0 charsOnLine := 0
lineHeightFixed := fixed.I(lineHeight)
drawer.Dot = fixed.P(0, lineHeight) drawer.Dot = fixed.P(0, lineHeight)
for _, g := range glyphs { for _, g := range glyphs {
@ -132,14 +152,14 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
bearingX := absFixedI26_6(gBounds.Min.X) bearingX := absFixedI26_6(gBounds.Min.X)
glyphWidth := float32((absFixedI26_6(gBounds.Max.X) - absFixedI26_6(gBounds.Min.X)).Ceil()) glyphWidth := float32((absFixedI26_6(gBounds.Max.X) - absFixedI26_6(gBounds.Min.X)).Ceil())
heightRounded := (ascent + descent).Ceil()
//TODO: Since sizeU/sizeV are now constant we should upload as a uniform
atlas.Glyphs[g] = FontAtlasGlyph{ atlas.Glyphs[g] = FontAtlasGlyph{
U: float32((drawer.Dot.X + bearingX).Floor()) / atlasSizeXF32, U: float32((drawer.Dot.X).Floor()) / atlasSizeXF32,
V: (atlasSizeYF32 - float32((drawer.Dot.Y + descent).Ceil())) / atlasSizeYF32, V: (atlasSizeYF32 - float32((drawer.Dot.Y).Ceil())) / atlasSizeYF32,
SizeU: glyphWidth / atlasSizeXF32, SizeU: advanceCeilF32 / atlasSizeXF32,
SizeV: float32(heightRounded) / atlasSizeYF32, SizeV: float32(lineHeight) / atlasSizeYF32,
Ascent: float32(ascent.Ceil()), Ascent: float32(ascent.Ceil()),
Descent: float32(descent.Ceil()), Descent: float32(descent.Ceil()),
@ -149,11 +169,19 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
Width: glyphWidth, Width: glyphWidth,
} }
// z := atlas.Glyphs[g] //Get glyph to draw but undo any applied descent so that the glyph is drawn sitting on the line exactly.
// fmt.Printf("c=%s; u=%f, v=%f, sizeU=%f, sizeV=%f; x=%d, y=%d, w=%f, h=%f\n", string(g), z.U, z.V, z.SizeU, z.SizeV, int(z.U*atlasSizeXF32), int(z.V*atlasSizeYF32), z.SizeU*atlasSizeXF32, z.SizeV*atlasSizeYF32) //Bearing will be applied correctly but descent will be the responsibility of the positioning code
imgRect, mask, maskp, _, _ := face.Glyph(drawer.Dot, g)
if imgRect.Max.Y > drawer.Dot.Y.Ceil() {
diff := imgRect.Max.Y - drawer.Dot.Y.Ceil()
imgRect.Min.Y -= diff
imgRect.Max.Y -= diff
}
drawer.DrawString(string(g)) //Draw glyph and advance dot
drawer.Dot.X += charPaddingXFixed // fmt.Println("G:", string(g), "Y:", drawer.Dot.Y.Ceil(), "; rect:", imgRect.String())
draw.DrawMask(drawer.Dst, imgRect, drawer.Src, image.Point{}, mask, maskp, draw.Over)
drawer.Dot.X += gAdvanceFixed + charPaddingXFixed
charsOnLine++ charsOnLine++
if charsOnLine == charsPerLine { if charsOnLine == charsPerLine {

View File

@ -62,9 +62,9 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
} }
gr.GlyphCount++ gr.GlyphCount++
glyphHeight := float32(g.Ascent + g.Descent) // glyphHeight := float32(g.Ascent + g.Descent)
scale := gglm.NewVec3(g.Width, glyphHeight, 1) scale := gglm.NewVec3(g.Advance, float32(gr.Atlas.LineHeight), 1)
// scale := gglm.NewVec3(g.Advance, glyphHeight, 1) // scale := gglm.NewVec3(g.Width, glyphHeight, 1)
//See: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png //See: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
//Quads are drawn from the center and so that's our baseline. But chars shouldn't be centered, they should follow ascent/decent/advance. //Quads are drawn from the center and so that's our baseline. But chars shouldn't be centered, they should follow ascent/decent/advance.
@ -73,8 +73,10 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
// //
//Horizontally the character should be drawn from the left edge not the center, so we just move it forward by advance/2 //Horizontally the character should be drawn from the left edge not the center, so we just move it forward by advance/2
drawPos := *pos drawPos := *pos
drawPos.SetX(drawPos.X() + g.BearingX) drawPos.SetX(drawPos.X())
drawPos.SetY(drawPos.Y() - g.Descent) drawPos.SetY(drawPos.Y() - g.Descent)
// drawPos.SetX(drawPos.X() + g.BearingX)
// drawPos.SetY(drawPos.Y() - g.Descent)
instancedData = append(instancedData, []float32{ instancedData = append(instancedData, []float32{
g.U, g.V, g.U, g.V,

View File

@ -168,7 +168,7 @@ func (p *program) Render() {
// p.GlyphRend.DrawTextOpenGL("y", gglm.NewVec3(0+xOff, 0+yOff, 0), textColor) // p.GlyphRend.DrawTextOpenGL("y", gglm.NewVec3(0+xOff, 0+yOff, 0), textColor)
// p.GlyphRend.DrawTextOpenGL("A\np-+_; This is", gglm.NewVec3(0.3+xOff, 0.5+yOff, 0), textColor) // p.GlyphRend.DrawTextOpenGL("A\np-+_; This is", gglm.NewVec3(0.3+xOff, 0.5+yOff, 0), textColor)
// p.GlyphRend.DrawTextOpenGLAbs("Hello there, friend.\nABCDEFGHIJKLMNOPQRSTUVWXYZ", gglm.NewVec3(0, 0, 0), textColor) // p.GlyphRend.DrawTextOpenGLAbs("Hello there, friend.\nABCDEFGHIJKLMNOPQRSTUVWXYZ", gglm.NewVec3(0, 0, 0), textColor)
p.GlyphRend.DrawTextOpenGLAbs(" Hello there, friend|.\n ABCDEFGHIJKLMNOPQRSTUVWXYZ", gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*2+yOff, 0), textColor) p.GlyphRend.DrawTextOpenGLAbs(" ijojo\n\n Hello there, friend|. pq?\n ABCDEFGHIJKLMNOPQRSTUVWXYZ", gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), textColor)
if p.shouldDrawGrid { if p.shouldDrawGrid {
p.drawGrid() p.drawGrid()