Skip to content

Commit

Permalink
add tolerance for text glyph curves
Browse files Browse the repository at this point in the history
  • Loading branch information
soypat committed Nov 24, 2024
1 parent 036b3b9 commit e113fdd
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 64 deletions.
57 changes: 20 additions & 37 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,26 @@ on:
branches: [ "main" ]

jobs:

build-windows:
name: Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.22

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...

build-mac:
name: MacOS
runs-on: macos-latest-xlarge
steps:
- uses: actions/checkout@v3

- name: Setup GPU drivers
run: brew install mesa

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.22

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
# Requires paying for GPU access.
# build-mac:
# name: MacOS
# runs-on: macos-latest-xlarge
# steps:
# - uses: actions/checkout@v3

# - name: Setup GPU drivers
# run: brew install mesa

# - name: Set up Go
# uses: actions/setup-go@v3
# with:
# go-version: 1.22

# - name: Build
# run: go build -v ./...

# - name: Test
# run: go test -v ./...

build-linux:
name: Linux
Expand Down
94 changes: 94 additions & 0 deletions examples/ui-geb/uigeb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"fmt"
"log"
"math"
"runtime"

"github.com/soypat/glgl/math/ms2"
"github.com/soypat/glgl/math/ms3"
"github.com/soypat/gsdf"
"github.com/soypat/gsdf/forge/textsdf"
"github.com/soypat/gsdf/glbuild"
"github.com/soypat/gsdf/gsdfaux"
)

func init() {
runtime.LockOSThread()
}

// scene generates the 3D object for rendering.
func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) {
// We create the cover of Gödel, Escher, Bach: an Eternal Golden Braid.
var f textsdf.Font
f.Configure(textsdf.FontConfig{
RelativeGlyphTolerance: 0.01,
})
err := f.LoadTTFBytes(textsdf.ISO3098TTF())
if err != nil {
return nil, err
}
G, _ := f.Glyph('G')
E, _ := f.Glyph('E')
B, _ := f.Glyph('B')

bbG := G.Bounds()
bbE := E.Bounds()
bbB := B.Bounds()
fmt.Println(bbG, bbE, bbB)
// return nil, errors.New("basdasd")
szG := bbG.Size()
szE := bbE.Size()
szB := bbB.Size()

// Match center between letters.
G = bld.Translate2D(G, -szG.X/2, -szG.Y/2)
E = bld.Translate2D(E, -szE.X/2, -szE.Y/2)
B = bld.Translate2D(B, -szB.X/2, -szB.Y/2)

// GEB size. Scale all letters to match size.
szz := ms2.MaxElem(szG, ms2.MaxElem(szE, szB)).Max()
sz := ms2.Vec{X: szz, Y: szz}
sclG := ms2.DivElem(sz, szG)
sclE := ms2.DivElem(sz, szE)
sclB := ms2.DivElem(sz, szB)
fmt.Println(sclG, sclE, sclB)
// Create 3D letters.
L := sz.Max()
G3 := bld.Extrude(G, L)
E3 := bld.Extrude(E, L)
B3 := bld.Extrude(B, L)

// Non-uniform scaling to fill letter intersections.
G3 = bld.Transform(G3, ms3.ScaleMat4(ms3.Vec{X: sclG.X, Y: sclG.Y, Z: 1}))
E3 = bld.Transform(E3, ms3.ScaleMat4(ms3.Vec{X: sclE.X, Y: sclE.Y, Z: 1}))
B3 = bld.Transform(B3, ms3.ScaleMat4(ms3.Vec{X: sclB.X, Y: sclB.Y, Z: 1}))

// Orient letters.
const deg90 = math.Pi / 2
E3 = bld.Rotate(E3, deg90, ms3.Vec{Y: 1})
B3 = bld.Rotate(B3, -deg90, ms3.Vec{X: 1})

GEB := bld.Intersection(G3, E3)
GEB = bld.Intersection(GEB, B3)
// return bld.Union(G3, E3, B3), bld.Err() // For debugging.
return GEB, bld.Err()
}

func main() {
var bld gsdf.Builder
shape, err := scene(&bld)
shape = bld.Scale(shape, 0.3)
if err != nil {
log.Fatal("creating scene:", err)
}
fmt.Println("Running UI... compiling text shaders may take a while...")
err = gsdfaux.UI(shape, gsdfaux.UIConfig{
Width: 800,
Height: 600,
})
if err != nil {
log.Fatal("UI:", err)
}
}
5 changes: 5 additions & 0 deletions examples/ui-text/uitext.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ func init() {
// scene generates the 3D object for rendering.
func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) {
var f textsdf.Font
f.Configure(textsdf.FontConfig{
RelativeGlyphTolerance: 0.15,
})
err := f.LoadTTFBytes(textsdf.ISO3098TTF())
if err != nil {
return nil, err
Expand All @@ -28,6 +31,8 @@ func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) {
if err != nil {
return nil, err
}
line = bld.Scale2D(line, 10) // Scale to prevent numeric error.

// Find characteristic size of characters(glyphs/letters).
len := utf8.RuneCountInString(text)
sz := line.Bounds().Size()
Expand Down
68 changes: 52 additions & 16 deletions forge/textsdf/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (
const firstBasic = '!'
const lastBasic = '~'

type FontConfig struct {
// RelativeGlyphTolerance sets the permissible curve tolerance for glyphs. Must be between 0..1. If zero a reasonable value is chosen.
RelativeGlyphTolerance float32
}

// Font implements font parsing and glyph (character) generation.
type Font struct {
ttf truetype.Font
Expand All @@ -25,6 +30,16 @@ type Font struct {
// Other kinds of glyphs.
otherGlyphs map[rune]glyph
bld gsdf.Builder
reltol float32 // Set by config or reset call if zeroed.
}

func (f *Font) Configure(cfg FontConfig) error {
if cfg.RelativeGlyphTolerance < 0 || cfg.RelativeGlyphTolerance >= 1 {
return errors.New("invalid RelativeGlyphTolerance")
}
f.reset()
f.reltol = cfg.RelativeGlyphTolerance
return nil
}

// LoadTTFBytes loads a TTF file blob into f. After calling Load the Font is ready to generate text SDFs.
Expand All @@ -50,6 +65,9 @@ func (f *Font) reset() {
delete(f.otherGlyphs, k)
}
}
if f.reltol == 0 {
f.reltol = 0.15
}
}

type glyph struct {
Expand All @@ -63,7 +81,8 @@ func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
var shapes []glbuild.Shader2D
scale := f.scale()
var idxPrev truetype.Index
var xOfs float32
var xOfs int64
scalout := f.scaleout()
for ic, c := range s {
if !unicode.IsGraphic(c) {
return nil, fmt.Errorf("char %q not graphic", c)
Expand All @@ -75,7 +94,7 @@ func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
if c == '\t' {
hm.AdvanceWidth *= 4
}
xOfs += float32(hm.AdvanceWidth)
xOfs += int64(hm.AdvanceWidth)
continue
}
charshape, err := f.Glyph(c)
Expand All @@ -84,14 +103,14 @@ func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
}

kern := f.ttf.Kern(scale, idxPrev, idx)
xOfs += float32(kern)
xOfs += int64(kern)
idxPrev = idx
if ic == 0 {
xOfs += float32(hm.LeftSideBearing)
xOfs += int64(hm.LeftSideBearing)
}
charshape = f.bld.Translate2D(charshape, xOfs, 0)
charshape = f.bld.Translate2D(charshape, float32(xOfs)*scalout, 0)
shapes = append(shapes, charshape)
xOfs += float32(hm.AdvanceWidth)
xOfs += int64(hm.AdvanceWidth)
}
if len(shapes) == 1 {
return shapes[0], nil
Expand Down Expand Up @@ -144,6 +163,21 @@ func (f *Font) scale() fixed.Int26_6 {
return fixed.Int26_6(f.ttf.FUnitsPerEm())
}

func (f *Font) rawbounds() ms2.Box {
bb := f.ttf.Bounds(f.scale())
return ms2.Box{
Min: ms2.Vec{X: float32(bb.Min.X), Y: float32(bb.Min.Y)},
Max: ms2.Vec{X: float32(bb.Max.X), Y: float32(bb.Max.Y)},
}
}

// scaleout defines the scaling from fixed point integers to
func (f *Font) scaleout() float32 {
bb := f.rawbounds()
sz := bb.Size().Min()
return 1. / float32(sz)
}

func (f *Font) makeGlyph(char rune) (glyph, error) {
g := &f.gb
bld := &f.bld
Expand All @@ -155,9 +189,11 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
if err != nil {
return glyph{}, err
}
scaleout := f.scaleout()

tol := f.reltol
// Build Glyph.
shape, fill, err := glyphCurve(bld, g.Points, 0, g.Ends[0])
shape, fill, err := glyphCurve(bld, g.Points, 0, g.Ends[0], tol, scaleout)
if err != nil {
return glyph{}, err
} else if !fill {
Expand All @@ -167,7 +203,7 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
start := g.Ends[0]
g.Ends = g.Ends[1:]
for _, end := range g.Ends {
sdf, fill, err := glyphCurve(bld, g.Points, start, end)
sdf, fill, err := glyphCurve(bld, g.Points, start, end, tol, scaleout)
start = end
if err != nil {
return glyph{}, err
Expand All @@ -181,20 +217,20 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
return glyph{sdf: shape}, nil
}

func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int) (glbuild.Shader2D, bool, error) {
func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int, tol, scale float32) (glbuild.Shader2D, bool, error) {
var (
sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: 0.1}
sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: tol}
sum float32
)
points = points[start:end]
n := len(points)
i := 0
var poly []ms2.Vec
vPrev := p2v(points[n-1])
vPrev := p2v(points[n-1], scale)
for i < n {
p0, p1, p2 := points[i], points[(i+1)%n], points[(i+2)%n]
onBits := onbits3(points, 0, n, i)
v0, v1, v2 := p2v(p0), p2v(p1), p2v(p2)
v0, v1, v2 := p2v(p0, scale), p2v(p1, scale), p2v(p2, scale)
implicit0 := ms2.Scale(0.5, ms2.Add(v0, v1))
implicit1 := ms2.Scale(0.5, ms2.Add(v1, v2))
switch onBits {
Expand Down Expand Up @@ -232,17 +268,17 @@ func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int) (glb
i += 2
}
poly = append(poly, v0) // Append start point.
poly = sampler.SampleBisect(poly, 1)
poly = sampler.SampleBisect(poly, 4)
sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
vPrev = v0
}
return bld.NewPolygon(poly), sum > 0, bld.Err()
}

func p2v(p truetype.Point) ms2.Vec {
func p2v(p truetype.Point, scale float32) ms2.Vec {
return ms2.Vec{
X: float32(p.X),
Y: float32(p.Y),
X: float32(p.X) * scale,
Y: float32(p.Y) * scale,
}
}

Expand Down
8 changes: 3 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ go 1.22.1
require (
github.com/chewxy/math32 v1.11.1
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
github.com/go-gl/glfw v0.0.0-20221017161538-93cebf72946b
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/soypat/glgl v0.0.0-20241121001014-cc8498d2a83d
golang.org/x/image v0.22.0
)

require (
github.com/go-gl/glfw v0.0.0-20221017161538-93cebf72946b // indirect
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 // indirect
golang.org/x/image v0.22.0 // indirect
)
require golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 // indirect
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOY
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/soypat/glgl v0.0.0-20241019203012-a2ad2ed164c2 h1:N/GkdilItOR9bBrGXIj+53DbxUzRnG1SF/aOkBQzlxs=
github.com/soypat/glgl v0.0.0-20241019203012-a2ad2ed164c2/go.mod h1:1LcEp6XHSMCI91WlJHzl/aW4Bp5v6yQOiYFyjrlk350=
github.com/soypat/glgl v0.0.0-20241117161642-84ce0213c9ea h1:bPlNmRe3fBJQPzqNWUN2ChFZJgGo+jk056+wpAzEX+w=
github.com/soypat/glgl v0.0.0-20241117161642-84ce0213c9ea/go.mod h1:1LcEp6XHSMCI91WlJHzl/aW4Bp5v6yQOiYFyjrlk350=
github.com/soypat/glgl v0.0.0-20241121001014-cc8498d2a83d h1:kDdWM661L/RAxg0j4gV+18hky7/3Tvbhd8O6p8CLB7w=
github.com/soypat/glgl v0.0.0-20241121001014-cc8498d2a83d/go.mod h1:1LcEp6XHSMCI91WlJHzl/aW4Bp5v6yQOiYFyjrlk350=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
Expand Down
Loading

0 comments on commit e113fdd

Please sign in to comment.