From 16bfe7f05bdab492adb7a90350e100784f8cb1df Mon Sep 17 00:00:00 2001 From: bloeys Date: Thu, 7 Jul 2022 15:33:51 +0400 Subject: [PATCH] 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. --- glyphs/font_atlas.go | 174 ++++++++++++++++++++++++++++------------- glyphs/glyphs.go | 91 ++++++++++++--------- imgui.ini | 5 ++ main.go | 14 +++- res/shaders/glyph.glsl | 10 +-- 5 files changed, 195 insertions(+), 99 deletions(-) create mode 100755 imgui.ini diff --git a/glyphs/font_atlas.go b/glyphs/font_atlas.go index ded0068..6a2ed89 100755 --- a/glyphs/font_atlas.go +++ b/glyphs/font_atlas.go @@ -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) diff --git a/glyphs/glyphs.go b/glyphs/glyphs.go index a52562f..da5fad4 100755 --- a/glyphs/glyphs.go +++ b/glyphs/glyphs.go @@ -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))) -} diff --git a/imgui.ini b/imgui.ini new file mode 100755 index 0000000..02e2318 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,5 @@ +[Window][Debug##Default] +Pos=814,53 +Size=405,63 +Collapsed=0 + diff --git a/main.go b/main.go index 4157194..e1f9a6f 100755 --- a/main.go +++ b/main.go @@ -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" diff --git a/res/shaders/glyph.glsl b/res/shaders/glyph.glsl index 8dbf9cf..5c69fdf 100755 --- a/res/shaders/glyph.glsl +++ b/res/shaders/glyph.glsl @@ -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);