From 254113c12e0bfa4a6aaaffc60d73af379d524db1 Mon Sep 17 00:00:00 2001 From: bloeys Date: Sun, 10 Jul 2022 21:27:23 +0400 Subject: [PATCH] Much tighter atlas packing --- glyphs/font_atlas.go | 166 +++++++++++++++++++++++++------------------ glyphs/glyphs.go | 10 +-- main.go | 2 +- 3 files changed, 103 insertions(+), 75 deletions(-) diff --git a/glyphs/font_atlas.go b/glyphs/font_atlas.go index ef6fda6..a59bfb7 100755 --- a/glyphs/font_atlas.go +++ b/glyphs/font_atlas.go @@ -6,7 +6,6 @@ import ( "image/color" "image/draw" "image/png" - "math" "os" "unicode" @@ -23,13 +22,13 @@ type FontAtlas struct { Img *image.RGBA Glyphs map[rune]FontAtlasGlyph - //Advance is global to the atlas because we only support monospaced fonts - Advance float32 - LineHeight float32 + //SpaceAdvance is global to the atlas because we only support monospaced fonts + SpaceAdvance float32 + LineHeight float32 } type FontAtlasGlyph struct { - R rune + Rune rune U float32 V float32 SizeU float32 @@ -63,6 +62,61 @@ func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*Font return NewFontAtlasFromFont(f, face, uint(fontOptions.Size)) } +func calcNeededAtlasSize(glyphs []rune, face font.Face, charPaddingXFixed, charPaddingYFixed fixed.Int26_6) (atlasSizeX, atlasSizeY int) { + + //Calculate needed atlas size + atlasSizeX = 512 + atlasSizeY = 512 + lineHeight := face.Metrics().Height + foundAtlasSize := false + for !foundAtlasSize { + + foundAtlasSize = true + dotX := charPaddingXFixed + dotY := lineHeight + atlasSizeXFixed := fixed.I(atlasSizeX) + atlasSizeYFixed := fixed.I(atlasSizeY) + for i := 0; i < len(glyphs); i++ { + + //Prepare all glyph metrics + g := glyphs[i] + gBounds, _, _ := face.GlyphBounds(g) + bearingXFixed := gBounds.Min.X + gWidthFixed := gBounds.Max.X - gBounds.Min.X + // descent := gBounds.Max.Y + + // Calculate distance dot will move after drawing. Advance normally if line has space, + // otherwise go to next line and reset X position. + distToMoveX := bearingXFixed + gWidthFixed + charPaddingXFixed + + //If bearing is negative this char might overlap with the previous one. + //So we need to move the dot so the drawer won't overlap even after a negative offset + if bearingXFixed < 0 { + distToMoveX += absI26_6(bearingXFixed) + } + + //If we hav eno more space go to next line + if dotX+distToMoveX >= atlasSizeXFixed { + + dotX = distToMoveX + dotY += lineHeight + charPaddingYFixed + + //If we have only one more empty line then resize to be safe against descents being clipped + if dotY+lineHeight >= atlasSizeYFixed { + atlasSizeX *= 2 + atlasSizeY *= 2 + foundAtlasSize = false + break + } + } else { + dotX += distToMoveX + } + } + } + + return atlasSizeX, atlasSizeY +} + //NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing //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. @@ -71,66 +125,31 @@ func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*Font //Only monospaced fonts are supported. func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) { + // Vertical padding must be a bit larger because low descent on one line + // and high ascent on the next might cause overlapping chars + const charPaddingXFixed = 4 << 6 + const charPaddingYFixed = 4 << 6 const maxAtlasSize = 8192 glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f)) assert.T(len(glyphs) > 0, "no glyphs") - //Find advance and line height - const charPaddingX = 4 - const charPaddingY = 4 - charAdvFixed, _ := face.GlyphAdvance('L') - charAdv := charAdvFixed.Ceil() + charPaddingX - - //Find largest vertical character. - //We don't use face.Metrics().Height because its not reliable - lineHeightFixed := fixed.Int26_6(0) - for _, g := range glyphs { - - gBounds, _, _ := face.GlyphBounds(g) - ascent := absI26_6(gBounds.Min.Y) - descent := absI26_6(gBounds.Max.Y) - - charHeight := ascent + descent - if charHeight > lineHeightFixed { - lineHeightFixed = charHeight - } - } - lineHeightFixed = fixed.I(lineHeightFixed.Ceil()) - lineHeight := lineHeightFixed.Ceil() - - //Calculate needed atlas size - atlasSizeX := 128 - atlasSizeY := 128 - - maxLinesInAtlas := atlasSizeY/lineHeight - 2 - charsPerLine := atlasSizeX/charAdv - 1 - linesNeeded := int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1 - - for linesNeeded > maxLinesInAtlas { - - atlasSizeX *= 2 - atlasSizeY *= 2 - - maxLinesInAtlas = atlasSizeY/lineHeight - 2 - - charsPerLine = atlasSizeX/charAdv - 1 - linesNeeded = int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1 - } - + atlasSizeX, atlasSizeY := calcNeededAtlasSize(glyphs, face, charPaddingXFixed, charPaddingYFixed) if atlasSizeX > maxAtlasSize { return nil, errors.New("atlas size went beyond the maximum of 8192*8192") } //Create atlas + lineHeight := face.Metrics().Height + spaceAdv, _ := face.GlyphAdvance(' ') atlas := &FontAtlas{ Font: f, Face: face, Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)), Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)), - Advance: float32(charAdv - charPaddingX), - LineHeight: float32(lineHeight), + SpaceAdvance: I26_6ToF32(spaceAdv), + LineHeight: I26_6ToF32(lineHeight), } //Clear background to black @@ -142,27 +161,46 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo } //Put glyphs on atlas - charPaddingXFixed := fixed.I(charPaddingX) - charPaddingYFixed := fixed.I(charPaddingY) - - charsOnLine := 0 - drawer.Dot = fixed.P(int(atlas.Advance+charPaddingX), lineHeight) + drawer.Dot = fixed.P(int(atlas.SpaceAdvance), 0) + drawer.Dot.X += charPaddingXFixed + drawer.Dot.Y = lineHeight const drawBoundingBoxes bool = false - for currGlyphCount, g := range glyphs { + atlasSizeXFixed := fixed.I(atlasSizeX) + // atlasSizeYFixed := fixed.I(atlasSizeY) + for _, g := range glyphs { + //Glyph metrics gBounds, gAdvanceFixed, _ := face.GlyphBounds(g) bearingXFixed := gBounds.Min.X ascentAbsFixed := absI26_6(gBounds.Min.Y) descentAbsFixed := absI26_6(gBounds.Max.Y) gWidthFixed := gBounds.Max.X - gBounds.Min.X - //If bearing is neagtive this char might overlap with the previous one. + //If bearing is negative this char might overlap with the previous one. //So we need to move the dot so the drawer won't overlap even after a negative offset if bearingXFixed < 0 { drawer.Dot.X += absI26_6(bearingXFixed) } + // Position dot by calculating how much it will move after drawing, and if there isn't enough space + // move to next line then draw + nextDotPosDeltaX := bearingXFixed + gWidthFixed + charPaddingXFixed + if drawer.Dot.X+nextDotPosDeltaX >= atlasSizeXFixed { + + drawer.Dot.X = charPaddingXFixed + if bearingXFixed < 0 { + drawer.Dot.X += absI26_6(bearingXFixed) + } + + drawer.Dot.Y += lineHeight + charPaddingYFixed + + // assert.T(drawer.Dot.Y+largestLineDescent+lineHeight < atlasSizeYFixed, "Failed to create atlas because it did not fit") + } + + drawer.Dot = fixed.P(drawer.Dot.X.Floor(), drawer.Dot.Y.Floor()) + + //Build and insert glyph struct gTopLeft := image.Point{ X: (drawer.Dot.X + bearingXFixed).Floor(), Y: (drawer.Dot.Y - ascentAbsFixed).Floor(), @@ -174,7 +212,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo } atlas.Glyphs[g] = FontAtlasGlyph{ - R: g, + Rune: g, U: float32(gTopLeft.X), V: float32(atlasSizeY - gBotRight.Y), SizeU: float32(gBotRight.X - gTopLeft.X), @@ -187,7 +225,6 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo } if consts.Mode_Debug && drawBoundingBoxes { - rect := image.Rectangle{ Min: gTopLeft, Max: gBotRight, @@ -195,19 +232,10 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo drawRectOutline(atlas.Img, rect, color.NRGBA{B: 255, A: 128}) } - //Draw glyph and advance dot + //Draw glyph imgRect, mask, maskp, _, _ := face.Glyph(drawer.Dot, g) draw.DrawMask(drawer.Dst, imgRect, drawer.Src, image.Point{}, mask, maskp, draw.Over) - - drawer.Dot.X += bearingXFixed + gWidthFixed + charPaddingXFixed - - charsOnLine++ - if charsOnLine == charsPerLine || currGlyphCount == len(glyphs)-1 { - - charsOnLine = 0 - drawer.Dot.X = fixed.I(int(atlas.Advance)) + charPaddingXFixed - drawer.Dot.Y += lineHeightFixed + charPaddingYFixed - } + drawer.Dot.X += nextDotPosDeltaX } // // This is a test section that uses the drawer to draw an Arabic diff --git a/glyphs/glyphs.go b/glyphs/glyphs.go index e8d8dfc..1b84ff5 100755 --- a/glyphs/glyphs.go +++ b/glyphs/glyphs.go @@ -110,10 +110,10 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos *pos = *screenPos.Clone() return } else if r == ' ' { - pos.AddX(gr.Atlas.Advance) + pos.AddX(gr.Atlas.SpaceAdvance) return } else if r == '\t' { - pos.AddX(gr.Atlas.Advance * float32(gr.SpacesPerTab)) + pos.AddX(gr.Atlas.SpaceAdvance * float32(gr.SpacesPerTab)) return } @@ -188,9 +188,9 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos } } -// func roundF32(x float32) float32 { -// return float32(math.Round(float64(x))) -// } +func roundF32(x float32) float32 { + return float32(math.Round(float64(x))) +} // func ceilF32(x float32) float32 { // return float32(math.Ceil(float64(x))) diff --git a/main.go b/main.go index 0008d17..9d48576 100755 --- a/main.go +++ b/main.go @@ -248,7 +248,7 @@ func (p *program) drawGrid() { sizeY := float32(p.GlyphRend.ScreenHeight) //columns - adv := p.GlyphRend.Atlas.Advance + adv := p.GlyphRend.Atlas.SpaceAdvance for i := int32(0); i < p.GlyphRend.ScreenWidth; i += int32(adv) { p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(float32(i)+0.5, sizeY/2, 0)).Scale(gglm.NewVec3(1, sizeY, 1)), p.gridMat) }