mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
Nice fonts without bleeding at all sizes using tiled atlas
This commit is contained in:
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
2
main.go
2
main.go
@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user