Horizontal position issues was because of the font! Also:

turn GlyphFromRunes into a function and use g.Advance
to have basic support for variable width fonts
This commit is contained in:
bloeys
2022-07-10 17:24:06 +04:00
parent dbd77e9ce7
commit c84702270e
6 changed files with 53 additions and 27 deletions

View File

@ -19,6 +19,7 @@ import (
type FontAtlas struct { type FontAtlas struct {
Font *truetype.Font Font *truetype.Font
Face font.Face
Img *image.RGBA Img *image.RGBA
Glyphs map[rune]FontAtlasGlyph Glyphs map[rune]FontAtlasGlyph
@ -28,6 +29,7 @@ type FontAtlas struct {
} }
type FontAtlasGlyph struct { type FontAtlasGlyph struct {
R rune
U float32 U float32
V float32 V float32
SizeU float32 SizeU float32
@ -123,6 +125,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
//Create atlas //Create atlas
atlas := &FontAtlas{ atlas := &FontAtlas{
Font: f, Font: f,
Face: face,
Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)), Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)), Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)),
@ -145,7 +148,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
charsOnLine := 0 charsOnLine := 0
drawer.Dot = fixed.P(int(atlas.Advance+charPaddingX), lineHeight) drawer.Dot = fixed.P(int(atlas.Advance+charPaddingX), lineHeight)
const drawBoundingBoxes bool = true const drawBoundingBoxes bool = false
for currGlyphCount, g := range glyphs { for currGlyphCount, g := range glyphs {
gBounds, gAdvanceFixed, _ := face.GlyphBounds(g) gBounds, gAdvanceFixed, _ := face.GlyphBounds(g)
@ -171,6 +174,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
} }
atlas.Glyphs[g] = FontAtlasGlyph{ atlas.Glyphs[g] = FontAtlasGlyph{
R: g,
U: float32(gTopLeft.X), U: float32(gTopLeft.X),
V: float32(atlasSizeY - gBotRight.Y), V: float32(atlasSizeY - gBotRight.Y),
SizeU: float32(gBotRight.X - gTopLeft.X), SizeU: float32(gBotRight.X - gTopLeft.X),
@ -206,6 +210,27 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
} }
} }
// // This is a test section that uses the drawer to draw an Arabic
// // string at the bottom of the atlas. Useful to compare glyph renderer against a 'correct' implementation
// str := "السلام عليكم"
// rs := []rune(str)
// finalR := make([]rune, 0)
// prevRune := invalidRune
// for i := len(rs) - 1; i >= 0; i-- {
// var g FontAtlasGlyph
// if i > 0 {
// //start or middle of sentence
// g = GlyphFromRunes(atlas.Glyphs, rs[i], rs[i-1], prevRune)
// } else {
// //Last character
// g = GlyphFromRunes(atlas.Glyphs, rs[i], invalidRune, prevRune)
// }
// prevRune = rs[i]
// finalR = append(finalR, g.R)
// }
// drawer.Dot.Y += lineHeightFixed + charPaddingYFixed
// drawer.DrawString(string(finalR))
return atlas, nil return atlas, nil
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/bloeys/nmage/buffers" "github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/materials" "github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes" "github.com/bloeys/nmage/meshes"
"github.com/bloeys/nterm/consts"
"github.com/go-gl/gl/v4.1-core/gl" "github.com/go-gl/gl/v4.1-core/gl"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
) )
@ -120,29 +121,29 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos
if run.IsLtr { if run.IsLtr {
if i < len(run.Runes)-1 { if i < len(run.Runes)-1 {
//start or middle of sentence //start or middle of sentence
g = gr.glyphFromRunes(r, prevRune, run.Runes[i+1]) g = GlyphFromRunes(gr.Atlas.Glyphs, r, prevRune, run.Runes[i+1])
} else { } else {
//Last character //Last character
g = gr.glyphFromRunes(r, prevRune, invalidRune) g = GlyphFromRunes(gr.Atlas.Glyphs, r, prevRune, invalidRune)
} }
} else { } else {
if i > 0 { if i > 0 {
//start or middle of sentence //start or middle of sentence
g = gr.glyphFromRunes(r, run.Runes[i-1], prevRune) g = GlyphFromRunes(gr.Atlas.Glyphs, r, run.Runes[i-1], prevRune)
} else { } else {
//Last character //Last character
g = gr.glyphFromRunes(r, invalidRune, prevRune) g = GlyphFromRunes(gr.Atlas.Glyphs, r, invalidRune, prevRune)
} }
} }
//We must adjust char positioning according to: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png //We must adjust char positioning according to: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
drawPos := *pos drawPos := *pos
//The flooring to an integer pixel must happen AFTER the (potentially) fractional adjustments have been made. //The flooring to an integer pixel must happen AFTER the (potentially) fractional adjustments have been made.
//This is what the truetype face.Rasterizer does and seems to give good results //This is what the truetype face.Rasterizer does and seems to give good results. Do NOT floor bearing/descent first.
drawPos.SetX(floorF32(drawPos.X() + g.BearingX)) drawPos.SetX(floorF32(drawPos.X() + g.BearingX))
drawPos.SetY(floorF32(drawPos.Y() - g.Descent)) drawPos.SetY(floorF32(drawPos.Y() - g.Descent))
if PrintPositions { if consts.Mode_Debug && PrintPositions {
oldXY := gglm.NewVec2(pos.X(), pos.Y()) oldXY := gglm.NewVec2(pos.X(), pos.Y())
newXY := gglm.NewVec2(drawPos.X(), drawPos.Y()) newXY := gglm.NewVec2(drawPos.X(), drawPos.Y())
fmt.Printf("char=%s; PosBefore=%s, PosAfter=%s; Bearing/Decent=(%f, %f)\n", string(r), oldXY.String(), newXY.String(), g.BearingX, g.Descent) fmt.Printf("char=%s; PosBefore=%s, PosAfter=%s; Bearing/Decent=(%f, %f)\n", string(r), oldXY.String(), newXY.String(), g.BearingX, g.Descent)
@ -177,24 +178,23 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos
gr.GlyphVBO[*bufIndex+1] = g.SizeV gr.GlyphVBO[*bufIndex+1] = g.SizeV
*bufIndex += 2 *bufIndex += 2
gr.GlyphCount++ pos.AddX(g.Advance)
pos.AddX(gr.Atlas.Advance)
// pos.AddX(g.Advance)
//If we fill the buffer we issue a draw call //If we fill the buffer we issue a draw call
gr.GlyphCount++
if gr.GlyphCount == MaxGlyphsPerBatch { if gr.GlyphCount == MaxGlyphsPerBatch {
gr.Draw() gr.Draw()
*bufIndex = 0 *bufIndex = 0
} }
} }
func roundF32(x float32) float32 { // func roundF32(x float32) float32 {
return float32(math.Round(float64(x))) // return float32(math.Round(float64(x)))
} // }
func ceilF32(x float32) float32 { // func ceilF32(x float32) float32 {
return float32(math.Ceil(float64(x))) // return float32(math.Ceil(float64(x)))
} // }
func floorF32(x float32) float32 { func floorF32(x float32) float32 {
return float32(math.Floor(float64(x))) return float32(math.Floor(float64(x)))
@ -270,7 +270,7 @@ func (gr *GlyphRend) GetTextRuns(t string, textRunsBuf *[]TextRun) {
} }
} }
func (gr *GlyphRend) glyphFromRunes(curr, prev, next rune) FontAtlasGlyph { func GlyphFromRunes(glyphTable map[rune]FontAtlasGlyph, curr, prev, next rune) FontAtlasGlyph {
//PERF: Map access times are absolute garbage to the point that ~85%+ of the runtime of this func //PERF: Map access times are absolute garbage to the point that ~85%+ of the runtime of this func
//is spent reading from maps :). Using nSet or fMap or similar would be a lot better. //is spent reading from maps :). Using nSet or fMap or similar would be a lot better.
@ -287,7 +287,7 @@ func (gr *GlyphRend) glyphFromRunes(curr, prev, next rune) FontAtlasGlyph {
//Isolated case //Isolated case
if !prevIsValid && !nextIsValid { if !prevIsValid && !nextIsValid {
g := gr.Atlas.Glyphs[curr] g := glyphTable[curr]
return g return g
} }
@ -366,8 +366,7 @@ func (gr *GlyphRend) glyphFromRunes(curr, prev, next rune) FontAtlasGlyph {
// } // }
} }
g := gr.Atlas.Glyphs[curr] return glyphTable[curr]
return g
} }
func (gr *GlyphRend) Draw() { func (gr *GlyphRend) Draw() {
@ -461,6 +460,14 @@ func (gr *GlyphRend) SetScreenSize(screenWidth, screenHeight int32) {
func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, screenHeight int32) (*GlyphRend, error) { func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, screenHeight int32) (*GlyphRend, error) {
var err error
if RuneInfos == nil {
RuneInfos, err = ParseUnicodeData("./unicode-data-13.txt", "./arabic-shaping-13.txt")
if err != nil {
return nil, err
}
}
gr := &GlyphRend{ gr := &GlyphRend{
GlyphCount: 0, GlyphCount: 0,
GlyphVBO: make([]float32, floatsPerGlyph*MaxGlyphsPerBatch), GlyphVBO: make([]float32, floatsPerGlyph*MaxGlyphsPerBatch),
@ -495,7 +502,6 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
gr.GlyphMat = materials.NewMaterial("glyphMat", "./res/shaders/glyph.glsl") gr.GlyphMat = materials.NewMaterial("glyphMat", "./res/shaders/glyph.glsl")
//With the material ready we can generate the atlas //With the material ready we can generate the atlas
var err error
gr.Atlas, err = NewFontAtlasFromFile(fontFile, fontOptions) gr.Atlas, err = NewFontAtlasFromFile(fontFile, fontOptions)
if err != nil { if err != nil {
return nil, err return nil, err
@ -563,10 +569,5 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
gr.SetScreenSize(screenWidth, screenHeight) gr.SetScreenSize(screenWidth, screenHeight)
RuneInfos, err = ParseUnicodeData("./unicode-data-13.txt", "./arabic-shaping-13.txt")
if err != nil {
return nil, err
}
return gr, nil return gr, nil
} }

View File

@ -94,7 +94,7 @@ func (p *program) Init() {
fmt.Printf("DPI: %f, font size: %d\n", dpi, p.FontSize) fmt.Printf("DPI: %f, font size: %d\n", dpi, p.FontSize)
w, h := p.win.SDLWin.GetSize() w, h := p.win.SDLWin.GetSize()
p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/KawkabMono-Regular.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h) p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/alm-fixed.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h)
if err != nil { if err != nil {
panic("Failed to create atlas from font file. Err: " + err.Error()) panic("Failed to create atlas from font file. Err: " + err.Error())
} }

Binary file not shown.

BIN
res/fonts/alm-fixed.ttf Executable file

Binary file not shown.

BIN
res/fonts/arabtype-variable.ttf Executable file

Binary file not shown.