mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
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:
@ -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
|
||||
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)
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user