Respect bearing,scaling and other metrics

This commit is contained in:
bloeys
2022-07-02 07:11:40 +04:00
parent 7a3ce51063
commit ad759e03fc
6 changed files with 288 additions and 102 deletions

View File

@ -1,11 +1,13 @@
package glyphs
import (
"errors"
"image"
"image/draw"
"image/png"
"math"
"os"
"unicode"
"github.com/bloeys/nterm/assert"
"github.com/golang/freetype/truetype"
@ -26,11 +28,17 @@ type FontAtlasGlyph struct {
SizeU float32
SizeV float32
Ascent float32
Descent float32
Advance float32
Ascent float32
Descent float32
Advance float32
BearingX float32
Width float32
}
//NewFontAtlasFromFile reads a TTF or TTC file and produces a font texture atlas containing
//all its characters using the specified options.
//
//Only monospaced fonts are supported
func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*FontAtlas, error) {
fBytes, err := os.ReadFile(fontFile)
@ -44,25 +52,28 @@ func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*Font
}
face := truetype.NewFace(f, fontOptions)
atlas := NewFontAtlasFromFont(f, face, uint(fontOptions.Size))
return atlas, nil
return NewFontAtlasFromFont(f, face, uint(fontOptions.Size))
}
func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) *FontAtlas {
//NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing
//all its characters using the specified options.
//
//Only monospaced fonts are supported
func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) {
const maxAtlasSize = 8192
glyphs := getGlyphsFromRanges(getGlyphRanges(f))
glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f))
assert.T(len(glyphs) > 0, "no glyphs")
//Choose atlas size
atlasSizeX := 512
atlasSizeY := 512
_, charWidthFixed, _ := face.GlyphBounds(glyphs[0])
charWidth := charWidthFixed.Floor()
lineHeight := face.Metrics().Height.Floor()
charWidthFixed, _ := face.GlyphAdvance('L')
charWidth := charWidthFixed.Ceil()
lineHeight := face.Metrics().Height.Ceil()
maxLinesInAtlas := atlasSizeY/lineHeight - 1
charsPerLine := atlasSizeX / charWidth
@ -78,7 +89,10 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) *Fon
charsPerLine = atlasSizeX / charWidth
linesNeeded = int(math.Ceil(float64(len(glyphs)) / float64(charsPerLine)))
}
assert.T(atlasSizeX <= maxAtlasSize, "Atlas size went beyond maximum")
if atlasSizeX > maxAtlasSize {
return nil, errors.New("atlas size went beyond the maximum of 8192*8192")
}
//Create atlas
atlas := &FontAtlas{
@ -102,28 +116,33 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) *Fon
atlasSizeYF32 := float32(atlasSizeY)
charsOnLine := 0
lineDx := fixed.P(0, lineHeight)
lineHeightFixed := fixed.I(lineHeight)
drawer.Dot = fixed.P(0, lineHeight)
for _, g := range glyphs {
gBounds, gAdvanceFixed, _ := face.GlyphBounds(g)
advanceCeilF32 := float32(gAdvanceFixed.Ceil())
descent := gBounds.Max.Y
advanceRoundedF32 := float32(gAdvanceFixed.Floor())
ascent := -gBounds.Min.Y
ascent := absFixedI26_6(gBounds.Min.Y)
descent := absFixedI26_6(gBounds.Max.Y)
bearingX := absFixedI26_6(gBounds.Min.X)
heightRounded := (ascent + descent).Floor()
glyphWidth := float32((absFixedI26_6(gBounds.Max.X - gBounds.Min.X)).Ceil())
heightRounded := (ascent + descent).Ceil()
atlas.Glyphs[g] = FontAtlasGlyph{
U: float32(drawer.Dot.X.Floor()) / atlasSizeXF32,
V: (atlasSizeYF32 - float32((drawer.Dot.Y + descent).Floor())) / atlasSizeYF32,
U: float32((drawer.Dot.X + bearingX).Floor()) / atlasSizeXF32,
V: (atlasSizeYF32 - float32((drawer.Dot.Y + descent).Ceil())) / atlasSizeYF32,
SizeU: advanceRoundedF32 / atlasSizeXF32,
SizeU: glyphWidth / atlasSizeXF32,
SizeV: float32(heightRounded) / atlasSizeYF32,
Ascent: float32(ascent.Floor()),
Descent: float32(descent.Floor()),
Advance: float32(advanceRoundedF32),
Ascent: float32(ascent.Ceil()),
Descent: float32(descent.Ceil()),
Advance: float32(advanceCeilF32),
BearingX: float32(bearingX.Ceil()),
Width: glyphWidth,
}
drawer.DrawString(string(g))
@ -132,11 +151,11 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) *Fon
charsOnLine = 0
drawer.Dot.X = 0
drawer.Dot = drawer.Dot.Add(lineDx)
drawer.Dot.Y += lineHeightFixed
}
}
return atlas
return atlas, nil
}
func SaveImgToPNG(img image.Image, file string) error {
@ -154,3 +173,60 @@ func SaveImgToPNG(img image.Image, file string) error {
return nil
}
//getGlyphRangesFromFont returns a list of ranges, each range is: [i][0]<=range<[i][1]
func getGlyphRangesFromFont(f *truetype.Font) (ret [][2]rune) {
isRuneInPrivateUseArea := func(r rune) bool {
return 0xe000 <= r && r <= 0xf8ff ||
0xf0000 <= r && r <= 0xffffd ||
0x100000 <= r && r <= 0x10fffd
}
rr := [2]rune{-1, -1}
for r := rune(0); r <= unicode.MaxRune; r++ {
if isRuneInPrivateUseArea(r) {
continue
}
if f.Index(r) == 0 {
continue
}
if rr[1] == r {
rr[1] = r + 1
continue
}
if rr[0] != -1 {
ret = append(ret, rr)
}
rr = [2]rune{r, r + 1}
}
if rr[0] != -1 {
ret = append(ret, rr)
}
return ret
}
//getGlyphsFromRuneRanges takes ranges of runes and produces an array of all the runes in these ranges
func getGlyphsFromRuneRanges(ranges [][2]rune) []rune {
out := make([]rune, 0)
for _, rr := range ranges {
temp := make([]rune, 0, rr[1]-rr[0])
for r := rr[0]; r < rr[1]; r++ {
temp = append(temp, r)
}
out = append(out, temp...)
}
return out
}
func absFixedI26_6(x fixed.Int26_6) fixed.Int26_6 {
if x < 0 {
return -x
}
return x
}

View File

@ -2,8 +2,8 @@ package glyphs
import (
"errors"
"math"
"os"
"unicode"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assets"
@ -29,67 +29,24 @@ type GlyphRend struct {
ScreenHeight int32
}
//getGlyphRanges returns a list of ranges, each range is: [i][0]<=range<[i][1]
func getGlyphRanges(f *truetype.Font) (ret [][2]rune) {
rr := [2]rune{-1, -1}
for r := rune(0); r <= unicode.MaxRune; r++ {
if privateUseArea(r) {
continue
}
if f.Index(r) == 0 {
continue
}
if rr[1] == r {
rr[1] = r + 1
continue
}
if rr[0] != -1 {
ret = append(ret, rr)
}
rr = [2]rune{r, r + 1}
}
if rr[0] != -1 {
ret = append(ret, rr)
}
return ret
}
func privateUseArea(r rune) bool {
return 0xe000 <= r && r <= 0xf8ff ||
0xf0000 <= r && r <= 0xffffd ||
0x100000 <= r && r <= 0x10fffd
}
//getGlyphsFromRanges takes ranges of runes and produces an array of all the runes in these ranges
func getGlyphsFromRanges(ranges [][2]rune) []rune {
out := make([]rune, 0)
for _, rr := range ranges {
temp := make([]rune, 0, rr[1]-rr[0])
for r := rr[0]; r < rr[1]; r++ {
temp = append(temp, r)
}
out = append(out, temp...)
}
return out
}
//DrawTextOpenGL prepares text that will be drawn on the next GlyphRend.Draw call.
//DrawTextOpenGLAbs prepares text that will be drawn on the next GlyphRend.Draw call.
//screenPos is in the range [0,1], where (0,0) is the bottom left.
//Color is RGBA in the range [0,1].
func (gr *GlyphRend) DrawTextOpenGL(text string, screenPos *gglm.Vec3, color *gglm.Vec4) {
func (gr *GlyphRend) DrawTextOpenGL01(text string, screenPos *gglm.Vec3, color *gglm.Vec4) {
screenPos.Set(screenPos.X()*float32(gr.ScreenWidth), screenPos.Y()*float32(gr.ScreenHeight), screenPos.Z())
gr.DrawTextOpenGLAbs(text, screenPos, color)
}
screenWidthF32 := float32(gr.ScreenWidth)
screenHeightF32 := float32(gr.ScreenHeight)
screenPos.Set(screenPos.X()*screenWidthF32, screenPos.Y()*screenHeightF32, screenPos.Z())
//DrawTextOpenGLAbs prepares text that will be drawn on the next GlyphRend.Draw call.
//screenPos is in the range ([0,ScreenWidth],[0,ScreenHeight]).
//Color is RGBA in the range [0,1].
func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color *gglm.Vec4) {
//Prepass to pre-allocate the buffer
rs := []rune(text)
const floatsPerGlyph = 18
// startPos := screenPos.Clone()
pos := screenPos.Clone()
instancedData := make([]float32, 0, len(rs)*floatsPerGlyph) //This a larger approximation than needed because we don't count spaces etc
for i := 0; i < len(rs); i++ {
@ -100,11 +57,15 @@ func (gr *GlyphRend) DrawTextOpenGL(text string, screenPos *gglm.Vec3, color *gg
screenPos.SetY(screenPos.Y() - float32(gr.Atlas.LineHeight))
pos = screenPos.Clone()
continue
} else if r == ' ' {
pos.SetX(pos.X() + g.Advance)
continue
}
gr.GlyphCount++
height := float32(g.Ascent + g.Descent)
scale := gglm.NewVec3(g.Advance, height, 1)
glyphHeight := float32(g.Ascent + g.Descent)
scale := gglm.NewVec3(g.Width, glyphHeight, 1)
// scale := gglm.NewVec3(g.Advance, glyphHeight, 1)
//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.
@ -113,8 +74,8 @@ func (gr *GlyphRend) DrawTextOpenGL(text string, screenPos *gglm.Vec3, color *gg
//
//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.SetX(drawPos.X() + g.Advance*0.5)
drawPos.SetY(drawPos.Y() + height*0.5 - g.Descent)
drawPos.SetX(drawPos.X() + g.BearingX)
drawPos.SetY(drawPos.Y() - g.Descent)
instancedData = append(instancedData, []float32{
g.U, g.V,
@ -123,13 +84,32 @@ func (gr *GlyphRend) DrawTextOpenGL(text string, screenPos *gglm.Vec3, color *gg
g.U + g.SizeU, g.V + g.SizeV,
color.R(), color.G(), color.B(), color.A(), //Color
drawPos.X(), drawPos.Y(), drawPos.Z(), //Model pos
roundF32(drawPos.X()), roundF32(drawPos.Y()), drawPos.Z(), //Model pos
scale.X(), scale.Y(), scale.Z(), //Model scale
}...)
pos.SetX(pos.X() + g.Advance)
}
//Draw baselines
// g := gr.Atlas.Glyphs['-']
// lineData := []float32{
// g.U, g.V,
// g.U + g.SizeU, g.V,
// g.U, g.V + g.SizeV,
// g.U + g.SizeU, g.V + g.SizeV,
// 1, 0, 0, 1, //Color
// 0, startPos.Y(), 1, //Model pos
// float32(gr.ScreenWidth), 5, 1, //Model scale
// }
// instancedData = append(instancedData, lineData...)
// lineData[13] -= float32(gr.Atlas.LineHeight)
// instancedData = append(instancedData, lineData...)
// gr.GlyphCount++
// gr.GlyphCount++
gr.GlyphVBO = append(gr.GlyphVBO, instancedData...)
}
@ -149,10 +129,19 @@ func (gr *GlyphRend) Draw() {
gr.GlyphVBO = []float32{}
}
func (gr *GlyphRend) SetFace(fontOptions *truetype.Options) {
//SetFace updates the underlying font atlas used by the glyph renderer.
//The current atlas is unchanged if there is an error
func (gr *GlyphRend) SetFace(fontOptions *truetype.Options) error {
face := truetype.NewFace(gr.Atlas.Font, fontOptions)
gr.Atlas = NewFontAtlasFromFont(gr.Atlas.Font, face, uint(fontOptions.Size))
newAtlas, err := NewFontAtlasFromFont(gr.Atlas.Font, face, uint(fontOptions.Size))
if err != nil {
return err
}
gr.Atlas = newAtlas
gr.updateFontAtlasTexture("temp-atlas")
return nil
}
func (gr *GlyphRend) SetFontFromFile(fontFile string, fontOptions *truetype.Options) error {
@ -210,9 +199,9 @@ func (gr *GlyphRend) SetScreenSize(screenWidth, screenHeight int32) {
gr.ScreenHeight = screenHeight
//The projection matrix fits the screen size. This is needed so we can size and position characters correctly.
projMtx := gglm.Ortho(0, float32(screenWidth), float32(screenHeight), 0, 0.1, 10)
viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -1), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0))
projViewMtx := projMtx.Clone().Mul(viewMtx)
projMtx := gglm.Ortho(0, float32(screenWidth), float32(screenHeight), 0, 0.1, 20)
viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -10), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0))
projViewMtx := projMtx.Mul(viewMtx)
gr.GlyphMat.DiffuseTex = gr.AtlasTex.TexID
gr.GlyphMat.SetUnifMat4("projViewMat", &projViewMtx.Mat4)
@ -246,11 +235,12 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
),
}
//The quad must be anchored at the bottom-left, not it's center (i.e. bottom-left vertex must be at 0,0)
gr.GlyphMesh.Buf.SetData([]float32{
-0.5, -0.5, 0,
0.5, -0.5, 0,
-0.5, 0.5, 0,
0.5, 0.5, 0,
0, 0, 0,
1, 0, 0,
0, 1, 0,
1, 1, 0,
})
gr.GlyphMesh.Buf.SetIndexBufData([]uint32{
@ -329,5 +319,10 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
gr.InstancedBuf.UnBind()
gr.SetScreenSize(screenWidth, screenHeight)
// fmt.Printf("lineHeight=%d, glyphInfo=%+v\n", gr.Atlas.LineHeight, gr.Atlas.Glyphs['A'])
return gr, nil
}
func roundF32(x float32) float32 {
return float32(math.Round(float64(x)))
}