Use per-char width instead of fixed advance in font atlas

This gives us more (much?) efficient packing of letters
and we load exact size of the letters.

It looks visually a lot better too!
This wasn't good with normal smapling, but using texelFetch its
great.
This commit is contained in:
bloeys
2022-07-07 15:33:51 +04:00
parent d23e833b54
commit 16bfe7f05b
5 changed files with 195 additions and 99 deletions

View File

@ -2,6 +2,7 @@ package glyphs
import (
"errors"
"fmt"
"image"
"image/color"
"image/draw"
@ -10,7 +11,6 @@ import (
"os"
"unicode"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nterm/assert"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
@ -25,12 +25,13 @@ type FontAtlas struct {
//Advance is global to the atlas because we only support monospaced fonts
Advance int
LineHeight int
SizeUV gglm.Vec2
}
type FontAtlasGlyph struct {
U float32
V float32
SizeU float32
SizeV float32
Ascent float32
Descent float32
@ -73,8 +74,8 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
assert.T(len(glyphs) > 0, "no glyphs")
//Find advance and line height
const charPaddingX = 2
const charPaddingY = 2
const charPaddingX = 4
const charPaddingY = 4
charAdvFixed, _ := face.GlyphAdvance('L')
charAdv := charAdvFixed.Ceil() + charPaddingX
@ -120,7 +121,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
//Create atlas
// atlasSizeXF32 := float32(atlasSizeX)
atlasSizeYF32 := float32(atlasSizeY)
// atlasSizeYF32 := float32(atlasSizeY)
atlas := &FontAtlas{
Font: f,
Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
@ -128,7 +129,6 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
Advance: charAdv - charPaddingX,
LineHeight: lineHeight,
SizeUV: *gglm.NewVec2(float32(charAdv-charPaddingX), float32(lineHeight)),
// SizeUV: *gglm.NewVec2(float32(charAdv-charPaddingX)/atlasSizeXF32, float32(lineHeight)/atlasSizeYF32),
}
@ -146,70 +146,58 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
charsOnLine := 0
drawer.Dot = fixed.P(atlas.Advance+charPaddingX, lineHeight)
const drawBoundingBoxes = false
drawHorizontalLines := true
drawVerticalLines := true
for _, g := range glyphs {
for currGlyphCount, g := range glyphs {
gBounds, _, _ := face.GlyphBounds(g)
ascent := absFixedI26_6(gBounds.Min.Y)
descent := absFixedI26_6(gBounds.Max.Y)
bearingX := absFixedI26_6(gBounds.Min.X)
bearingX := gBounds.Min.X
ascentAbsFixed := absFixedI26_6(gBounds.Min.Y)
descentAbsFixed := absFixedI26_6(gBounds.Max.Y)
gWidth := gBounds.Min.X + gBounds.Max.X
gTopLeft := image.Point{
X: (drawer.Dot.X + bearingX).Floor(),
Y: (drawer.Dot.Y - ascentAbsFixed).Floor(),
}
gBotRight := image.Point{
X: (drawer.Dot.X + gWidth).Ceil(),
Y: (drawer.Dot.Y + descentAbsFixed).Ceil(),
}
atlas.Glyphs[g] = FontAtlasGlyph{
U: float32((drawer.Dot.X).Floor()),
V: (atlasSizeYF32 - float32((drawer.Dot.Y).Ceil())),
// U: float32((drawer.Dot.X).Floor()) / atlasSizeXF32,
// V: (atlasSizeYF32 - float32((drawer.Dot.Y).Ceil())) / atlasSizeYF32,
U: float32(gTopLeft.X) - 1,
V: float32(atlasSizeY - gBotRight.Y),
SizeU: float32(gBotRight.X - gTopLeft.X),
SizeV: float32(gBotRight.Y - gTopLeft.Y),
Ascent: float32(ascent.Ceil()),
Descent: float32(descent.Ceil()),
Ascent: float32(ascentAbsFixed.Ceil()),
Descent: float32(descentAbsFixed.Ceil()),
BearingX: float32(bearingX.Ceil()),
}
//Get glyph to draw but undo any applied descent so that the glyph is drawn sitting on the line exactly.
//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
imgRect, mask, maskp, gAdvance, _ := face.Glyph(drawer.Dot, g)
if gAdvance == 0 {
fmt.Printf("Got advance of %s for char with code 0x%04x\n", gAdvance.String(), g)
continue
}
if drawVerticalLines {
rectCopy := imgRect
rectCopy.Min.Y = 0
rectCopy.Max.Y = drawer.Dst.Bounds().Max.Y
rectCopy.Max.X = rectCopy.Min.X + 1
oldPos := drawer.Dot
drawer.Dot.Y = 0
// fmt.Printf("Drawing with maskP %s\n", maskp.String())
draw.Draw(drawer.Dst, rectCopy, image.NewUniform(color.NRGBA{G: 255, A: 255}), image.Point{}, draw.Over)
drawer.Dot = oldPos
if drawBoundingBoxes {
rect := image.Rectangle{
Min: gTopLeft,
Max: gBotRight,
}
drawRectOutline(atlas.Img, rect, color.NRGBA{B: 255, A: 128})
}
//Draw glyph and advance dot
draw.DrawMask(drawer.Dst, imgRect, drawer.Src, image.Point{}, mask, maskp, draw.Over)
drawer.Dot.X += fixed.I(atlas.Advance) + charPaddingXFixed
drawer.Dot.X += gWidth + charPaddingXFixed
charsOnLine++
if charsOnLine == charsPerLine {
if drawHorizontalLines {
rectCopy := imgRect
rectCopy.Min.X = 0
rectCopy.Max.X = drawer.Dst.Bounds().Max.X
// rectCopy.Min.Y += (lineHeightFixed + charPaddingYFixed).Floor() * 1
rectCopy.Max.Y = rectCopy.Min.Y + 1
oldPos := drawer.Dot
drawer.Dot.X = 0
draw.Draw(drawer.Dst, rectCopy, image.NewUniform(color.NRGBA{G: 255, A: 255}), image.Point{}, draw.Over)
drawer.Dot = oldPos
drawVerticalLines = false
}
if charsOnLine == charsPerLine || currGlyphCount == len(glyphs)-1 {
charsOnLine = 0
drawer.Dot.X = fixed.I(atlas.Advance) + charPaddingXFixed
@ -220,6 +208,82 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
return atlas, nil
}
func drawRectOutline(img *image.RGBA, rect image.Rectangle, color color.NRGBA) {
rowPixCount := img.Stride / 4
topLeft := img.PixOffset(rect.Min.X, rect.Min.Y)
botRight := img.PixOffset(rect.Max.X, rect.Max.Y)
for i := topLeft; i <= botRight; i += 4 {
pixel := i / 4
y := pixel / rowPixCount
x := pixel - y*rowPixCount
if x >= rect.Min.X && x <= rect.Max.X && y >= rect.Min.Y && y <= rect.Max.Y {
if x == rect.Min.X || x == rect.Max.X || y == rect.Min.Y || y == rect.Max.Y {
img.Pix[i+3] = color.A
img.Pix[i+2] = color.B
img.Pix[i+1] = color.G
img.Pix[i+0] = color.R
}
}
}
}
func DrawRect(img *image.RGBA, rect image.Rectangle, color color.NRGBA) {
rowPixCount := img.Stride / 4
topLeft := img.PixOffset(rect.Min.X, rect.Min.Y)
botRight := img.PixOffset(rect.Max.X, rect.Max.Y)
//Draw top line
for i := topLeft; i <= botRight; i += 4 {
pixel := i / 4
y := pixel / rowPixCount
x := pixel - y*rowPixCount
if x >= rect.Min.X && x <= rect.Max.X && y >= rect.Min.Y && y <= rect.Max.Y {
img.Pix[i+3] = color.A
img.Pix[i+2] = color.B
img.Pix[i+1] = color.G
img.Pix[i+0] = color.R
}
}
}
func DrawVerticalLine(img *image.RGBA, posX int, color color.NRGBA) {
rowLength := img.Stride
start := img.PixOffset(posX, 0)
for i := start; i < len(img.Pix); i += rowLength {
img.Pix[i+3] = color.A
img.Pix[i+2] = color.B
img.Pix[i+1] = color.G
img.Pix[i+0] = color.R
}
}
func DrawHorizontalLine(img *image.RGBA, posY int, color color.NRGBA) {
rowLength := img.Stride
start := img.PixOffset(0, posY)
//Horizontal line
for i := start; i < start+rowLength; i += 4 {
img.Pix[i+3] = color.A
img.Pix[i+2] = color.B
img.Pix[i+1] = color.G
img.Pix[i+0] = color.R
}
}
func SaveImgToPNG(img image.Image, file string) error {
outFile, err := os.Create(file)

View File

@ -2,7 +2,6 @@ package glyphs
import (
"errors"
"math"
"unicode"
"github.com/bloeys/gglm/gglm"
@ -17,7 +16,7 @@ import (
const (
MaxGlyphsPerBatch = 16384
floatsPerGlyph = 11
floatsPerGlyph = 13
invalidRune = unicode.ReplacementChar
)
@ -69,7 +68,7 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
pos := screenPos.Clone()
advanceF32 := float32(gr.Atlas.Advance)
lineHeightF32 := float32(gr.Atlas.LineHeight)
scale := gglm.NewVec2(advanceF32, lineHeightF32)
// scale := gglm.NewVec2(advanceF32, lineHeightF32)
buffIndex := gr.GlyphCount * floatsPerGlyph
@ -122,28 +121,37 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
//The uvs coming in make it so that glyphs are sitting on top of the baseline (no descent) and with horizontal bearing applied.
//So to position correctly we move them down by the descent amount.
drawPos := *pos
drawPos.SetX(drawPos.X())
drawPos.SetX(drawPos.X() + g.BearingX)
drawPos.SetY(drawPos.Y() - g.Descent)
//Add the glyph information to the vbo
//UV
gr.GlyphVBO[buffIndex+0] = g.U
gr.GlyphVBO[buffIndex+1] = g.V
buffIndex += 2
//UVSize
gr.GlyphVBO[buffIndex+0] = g.SizeU
gr.GlyphVBO[buffIndex+1] = g.SizeV
buffIndex += 2
//Color
gr.GlyphVBO[buffIndex+2] = color.R()
gr.GlyphVBO[buffIndex+3] = color.G()
gr.GlyphVBO[buffIndex+4] = color.B()
gr.GlyphVBO[buffIndex+5] = color.A()
gr.GlyphVBO[buffIndex+0] = color.R()
gr.GlyphVBO[buffIndex+1] = color.G()
gr.GlyphVBO[buffIndex+2] = color.B()
gr.GlyphVBO[buffIndex+3] = color.A()
buffIndex += 4
//Model Pos
gr.GlyphVBO[buffIndex+6] = drawPos.X()
gr.GlyphVBO[buffIndex+7] = drawPos.Y()
gr.GlyphVBO[buffIndex+8] = drawPos.Z()
gr.GlyphVBO[buffIndex+0] = drawPos.X()
gr.GlyphVBO[buffIndex+1] = drawPos.Y()
gr.GlyphVBO[buffIndex+2] = drawPos.Z()
buffIndex += 3
//Model Scale
gr.GlyphVBO[buffIndex+9] = scale.X()
gr.GlyphVBO[buffIndex+10] = scale.Y()
gr.GlyphVBO[buffIndex+0] = g.SizeU
gr.GlyphVBO[buffIndex+1] = g.SizeV
buffIndex += 2
gr.GlyphCount++
pos.AddX(advanceF32)
@ -152,8 +160,6 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
if gr.GlyphCount == MaxGlyphsPerBatch {
gr.Draw()
buffIndex = 0
} else {
buffIndex += floatsPerGlyph
}
prevRune = r
@ -192,28 +198,37 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
//The uvs coming in make it so that glyphs are sitting on top of the baseline (no descent) and with horizontal bearing applied.
//So to position correctly we move them down by the descent amount.
drawPos := *pos
drawPos.SetX(drawPos.X())
drawPos.SetX(drawPos.X() + g.BearingX)
drawPos.SetY(drawPos.Y() - g.Descent)
//Add the glyph information to the vbo
//UV
gr.GlyphVBO[buffIndex+0] = g.U
gr.GlyphVBO[buffIndex+1] = g.V
buffIndex += 2
//UVSize
gr.GlyphVBO[buffIndex+0] = g.SizeU
gr.GlyphVBO[buffIndex+1] = g.SizeV
buffIndex += 2
//Color
gr.GlyphVBO[buffIndex+2] = color.R()
gr.GlyphVBO[buffIndex+3] = color.G()
gr.GlyphVBO[buffIndex+4] = color.B()
gr.GlyphVBO[buffIndex+5] = color.A()
gr.GlyphVBO[buffIndex+0] = color.R()
gr.GlyphVBO[buffIndex+1] = color.G()
gr.GlyphVBO[buffIndex+2] = color.B()
gr.GlyphVBO[buffIndex+3] = color.A()
buffIndex += 4
//Model Pos
gr.GlyphVBO[buffIndex+6] = roundF32(drawPos.X())
gr.GlyphVBO[buffIndex+7] = roundF32(drawPos.Y())
gr.GlyphVBO[buffIndex+8] = drawPos.Z()
gr.GlyphVBO[buffIndex+0] = drawPos.X()
gr.GlyphVBO[buffIndex+1] = drawPos.Y()
gr.GlyphVBO[buffIndex+2] = drawPos.Z()
buffIndex += 3
//Model Scale
gr.GlyphVBO[buffIndex+9] = scale.X()
gr.GlyphVBO[buffIndex+10] = scale.Y()
gr.GlyphVBO[buffIndex+0] = g.SizeU
gr.GlyphVBO[buffIndex+1] = g.SizeV
buffIndex += 2
gr.GlyphCount++
pos.AddX(advanceF32)
@ -222,8 +237,6 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
if gr.GlyphCount == MaxGlyphsPerBatch {
gr.Draw()
buffIndex = 0
} else {
buffIndex += floatsPerGlyph
}
prevRune = r
@ -450,7 +463,7 @@ func (gr *GlyphRend) updateFontAtlasTexture() error {
//Update material
gr.GlyphMat.DiffuseTex = gr.AtlasTex.TexID
gr.GlyphMat.SetUnifVec2("sizeUV", &gr.Atlas.SizeUV)
// gr.GlyphMat.SetUnifVec2("sizeUV", &gr.Atlas.SizeUV)
return nil
}
@ -527,6 +540,7 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
gr.InstancedBuf.SetLayout(
buffers.Element{ElementType: buffers.DataTypeVec2}, //UV0
buffers.Element{ElementType: buffers.DataTypeVec2}, //UVSize
buffers.Element{ElementType: buffers.DataTypeVec4}, //Color
buffers.Element{ElementType: buffers.DataTypeVec3}, //ModelPos
buffers.Element{ElementType: buffers.DataTypeVec2}, //ModelScale
@ -542,21 +556,26 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
gl.VertexAttribPointer(1, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(uvEle.Offset))
gl.VertexAttribDivisor(1, 1)
colorEle := layout[1]
uvSize := layout[1]
gl.EnableVertexAttribArray(2)
gl.VertexAttribPointer(2, colorEle.ElementType.CompCount(), colorEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(colorEle.Offset))
gl.VertexAttribPointer(2, uvSize.ElementType.CompCount(), uvSize.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(uvSize.Offset))
gl.VertexAttribDivisor(2, 1)
posEle := layout[2]
colorEle := layout[2]
gl.EnableVertexAttribArray(3)
gl.VertexAttribPointer(3, posEle.ElementType.CompCount(), posEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(posEle.Offset))
gl.VertexAttribPointer(3, colorEle.ElementType.CompCount(), colorEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(colorEle.Offset))
gl.VertexAttribDivisor(3, 1)
scaleEle := layout[3]
posEle := layout[3]
gl.EnableVertexAttribArray(4)
gl.VertexAttribPointer(4, scaleEle.ElementType.CompCount(), scaleEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(scaleEle.Offset))
gl.VertexAttribPointer(4, posEle.ElementType.CompCount(), posEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(posEle.Offset))
gl.VertexAttribDivisor(4, 1)
scaleEle := layout[4]
gl.EnableVertexAttribArray(5)
gl.VertexAttribPointer(5, scaleEle.ElementType.CompCount(), scaleEle.ElementType.GLType(), false, gr.InstancedBuf.Stride, gl.PtrOffset(scaleEle.Offset))
gl.VertexAttribDivisor(5, 1)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gr.InstancedBuf.UnBind()
@ -572,7 +591,3 @@ func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, s
return gr, nil
}
func roundF32(x float32) float32 {
return float32(math.Round(float64(x)))
}

5
imgui.ini Executable file
View File

@ -0,0 +1,5 @@
[Window][Debug##Default]
Pos=814,53
Size=405,63
Collapsed=0

14
main.go
View File

@ -11,8 +11,10 @@ import (
"github.com/bloeys/nmage/meshes"
"github.com/bloeys/nmage/renderer/rend3dgl"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/bloeys/nterm/assert"
"github.com/bloeys/nterm/glyphs"
"github.com/golang/freetype/truetype"
"github.com/inkyblackness/imgui-go/v4"
"github.com/veandco/go-sdl2/sdl"
"golang.org/x/image/font"
)
@ -107,6 +109,7 @@ func (p *program) Init() {
p.handleWindowResize()
// runs := p.GlyphRend.GetTextRuns("hello there يا friend. أسمي عمر wow")
// runs := p.GlyphRend.GetTextRuns("hello there my friend!")
// fmt.Printf("%+v\n", runs)
// for _, r := range runs {
// fmt.Printf("%s;\n", string(r))
@ -164,8 +167,15 @@ func (p *program) Update() {
if input.KeyClicked(sdl.K_SPACE) {
p.shouldDrawGrid = !p.shouldDrawGrid
}
imgui.InputText("", &textToShow)
if len(textToShow) > 0 {
assert.T(len(textToShow) == len(p.GlyphRend.GetTextRuns(textToShow)[0]), "??")
}
}
var textToShow = "Hello there my friend"
var xOff float32 = 0
var yOff float32 = 0
@ -198,8 +208,10 @@ func (p *program) Render() {
}
textColor := gglm.NewVec4(r, g, b, 1)
str := textToShow
// str := "مرحبا بك my friend"
str := " hello there يا friend. سمي عمر wow"
// str := "my, friend"
// str := " hello there يا friend. سمي عمر wow"
// str := " ijojo\n\n Hello there, friend|. pq?\n ABCDEFG\tHIJKLMNOPQRSTUVWXYZ\nمرحبا بك"
// str := " ijojo\n\n Hello there, friend|. pq?\n ABCDEFG\tHIJKLMNOPQRSTUVWXYZ"

View File

@ -6,9 +6,10 @@ layout(location=0) in vec3 aVertPos;
//Instanced
layout(location=1) in vec2 aUV0;
layout(location=2) in vec4 aVertColor;
layout(location=3) in vec3 aModelPos;
layout(location=4) in vec2 aModelScale;
layout(location=2) in vec2 aUVSize;
layout(location=3) in vec4 aVertColor;
layout(location=4) in vec3 aModelPos;
layout(location=5) in vec2 aModelScale;
out vec2 v2fUV0;
out vec4 v2fColor;
@ -16,7 +17,6 @@ out vec3 v2fFragPos;
uniform mat4 projViewMat;
uniform vec2 modelSize;
uniform vec2 sizeUV;
void main()
{
@ -27,7 +27,7 @@ void main()
aModelPos.x, aModelPos.y, aModelPos.z, 1.0
);
v2fUV0 = aUV0 + aVertPos.xy * sizeUV;
v2fUV0 = aUV0 + aVertPos.xy * aUVSize;
v2fColor = aVertColor;
gl_Position = projViewMat * modelMat * vec4(aVertPos, 1.0);