diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 44208b4..eb24f97 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 diff --git a/examples/ui-geb/uigeb.go b/examples/ui-geb/uigeb.go new file mode 100644 index 0000000..f09a69a --- /dev/null +++ b/examples/ui-geb/uigeb.go @@ -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) + } +} diff --git a/examples/ui-text/uitext.go b/examples/ui-text/uitext.go index 1c3ba44..211354c 100644 --- a/examples/ui-text/uitext.go +++ b/examples/ui-text/uitext.go @@ -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 @@ -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() diff --git a/forge/textsdf/font.go b/forge/textsdf/font.go index 309c5ff..063793a 100644 --- a/forge/textsdf/font.go +++ b/forge/textsdf/font.go @@ -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 @@ -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. @@ -50,6 +65,9 @@ func (f *Font) reset() { delete(f.otherGlyphs, k) } } + if f.reltol == 0 { + f.reltol = 0.15 + } } type glyph struct { @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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 { @@ -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, } } diff --git a/go.mod b/go.mod index dfa4ee0..2d31fb4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 5e3eab8..147c78f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/gsdf2d.go b/gsdf2d.go index e2fff5e..0c5835e 100644 --- a/gsdf2d.go +++ b/gsdf2d.go @@ -1235,3 +1235,40 @@ func (ca *circarray2D) AppendShaderBody(b []byte) []byte { func (u *circarray2D) AppendShaderObjects(objects []glbuild.ShaderObject) []glbuild.ShaderObject { return objects } + +// ScaleXY scales s by scaleFactor around the origin. +func (bld *Builder) Scale2D(s glbuild.Shader2D, scale float32) glbuild.Shader2D { + return &scale2D{s: s, scale: scale} +} + +type scale2D struct { + s glbuild.Shader2D + scale float32 +} + +func (u *scale2D) Bounds() ms2.Box { + b := u.s.Bounds() + return b.Scale(ms2.Vec{X: u.scale, Y: u.scale}) +} + +func (s *scale2D) ForEach2DChild(userData any, fn func(userData any, s *glbuild.Shader2D) error) error { + return fn(userData, &s.s) +} + +func (s *scale2D) AppendShaderName(b []byte) []byte { + b = append(b, "scalexy_"...) + b = s.s.AppendShaderName(b) + return b +} + +func (s *scale2D) AppendShaderBody(b []byte) []byte { + b = glbuild.AppendFloatDecl(b, "s", s.scale) + b = append(b, "return "...) + b = s.s.AppendShaderName(b) + b = append(b, "(p/s)*s;"...) + return b +} + +func (u *scale2D) AppendShaderObjects(objects []glbuild.ShaderObject) []glbuild.ShaderObject { + return objects +} diff --git a/gsdfaux/gsdfaux.go b/gsdfaux/gsdfaux.go index 6070350..e3e7c64 100644 --- a/gsdfaux/gsdfaux.go +++ b/gsdfaux/gsdfaux.go @@ -39,7 +39,7 @@ type RenderConfig struct { type UIConfig struct { Width, Height int - Ctx context.Context + Context context.Context } func UI(s glbuild.Shader3D, cfg UIConfig) error { diff --git a/gsdfaux/ui.go b/gsdfaux/ui.go index 5cff5c7..e5e4971 100644 --- a/gsdfaux/ui.go +++ b/gsdfaux/ui.go @@ -169,13 +169,20 @@ void main() { isMousePressed = false window.SetInputMode(glfw.CursorMode, glfw.CursorNormal) } - } }) // Main render loop previousTime := glfw.GetTime() + ctx := cfg.Context for !window.ShouldClose() { + if ctx != nil { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + } width, height := window.GetSize() currentTime := glfw.GetTime() elapsedTime := currentTime - previousTime