diff --git a/go.mod b/go.mod index b8914bd5d..07dbb7fd5 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( ) require ( - github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9 // indirect github.com/jfreymuth/vorbis v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect golang.org/x/mod v0.19.0 // indirect diff --git a/go.sum b/go.sum index 44de873b2..a111c4bd6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,3 @@ -github.com/Zyko0/Ebiary/atlas v0.0.0-20240715185308-15c9f3ed18e4 h1:hw0xjh6636KyizJh10Akyym5q+gJT/JZaG5YoOECpCo= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240715185308-15c9f3ed18e4/go.mod h1:TqaiWLulZjjwPydGAqz6EqHELgtnn/swD/0PlPovCp8= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727132256-058f84c22395 h1:4kBG4iBHZD8ZnwzYarhCYhBP3bTwTRXHYqjo/TxAvUI= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727132256-058f84c22395/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727143531-e678f4f326f6 h1:sRjhOIw0+bqFvKOw+cUmIw0LFkhbo95KlKeIDupVp6c= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727143531-e678f4f326f6/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727145901-a622e72da2b1 h1:Tv7NzEiyRLfo2PJNo+IQRf0VdCskblash/RGnSZsQJQ= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727145901-a622e72da2b1/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9 h1:qfCLi8fCRFO3zVs9c60Ey2xJa+8VfVRKJlgbQVjYfpk= -github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= @@ -21,6 +11,7 @@ github.com/gen2brain/mpeg v0.3.2-0.20240412154320-a2ac4fc8a46f/go.mod h1:i/ebyRR github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.3 h1:gSxacUXbVZcffXOHl/BWGvf61LpJYLoEaZlsiLlWXPk= diff --git a/text/v2/atlas.go b/text/v2/atlas.go index 733c99d4a..0c2e70565 100644 --- a/text/v2/atlas.go +++ b/text/v2/atlas.go @@ -1,36 +1,282 @@ package text import ( - "github.com/Zyko0/Ebiary/atlas" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/internal/packing" ) type glyphAtlas struct { - atlas *atlas.Atlas + page *packing.Page + image *ebiten.Image +} + +type glyphImage struct { + atlas *glyphAtlas + node *packing.Node +} + +func (i *glyphImage) Image() *ebiten.Image { + return i.atlas.image.SubImage(i.node.Region()).(*ebiten.Image) } func newGlyphAtlas() *glyphAtlas { return &glyphAtlas{ // Note: 128x128 is arbitrary, maybe a better value can be inferred // from the font size or something - atlas: atlas.New(128, 128, nil), + page: packing.NewPage(128, 128, 1024), // TODO: not 1024 + image: ebiten.NewImage(128, 128), } } -func (g *glyphAtlas) NewImage(w, h int) *atlas.Image { - if img := g.atlas.NewImage(w, h); img != nil { - return img +func (g *glyphAtlas) NewImage(w, h int) *glyphImage { + n := g.page.Alloc(w, h) + pw, ph := g.page.Size() + if pw > g.image.Bounds().Dx() || ph > g.image.Bounds().Dy() { + newImage := ebiten.NewImage(pw, ph) + newImage.DrawImage(g.image, nil) + g.image = newImage } - // Grow atlas - old := g.atlas.Image() - - aw, ah := g.atlas.Bounds().Dx()*2, g.atlas.Bounds().Dy()*2 - g.atlas = atlas.New(aw, ah, nil) - g.atlas.Image().DrawImage(old, nil) - - return g.NewImage(w, h) + return &glyphImage{ + atlas: g, + node: n, + } } -func (g *glyphAtlas) Free(img *atlas.Image) { - g.atlas.Free(img) +func (g *glyphAtlas) Free(img *glyphImage) { + g.page.Free(img.node) } + + +type drawRange struct { + atlas *glyphAtlas + end int +} + +// drawList stores triangle versions of DrawImage calls when +// all images are sub-images of an atlas. +// Temporary vertices and indices can be re-used after calling +// Flush, so it is more efficient to keep a reference to a drawList +// instead of creating a new one every frame. +type drawList struct { + ranges []drawRange + vx []ebiten.Vertex + ix []uint16 +} + +// drawCommand is the equivalent of the regular DrawImageOptions +// but only including options that will not break batching. +// Filter, Address, Blend and AntiAlias are determined at Flush() +type drawCommand struct { + Image *glyphImage + + ColorScale ebiten.ColorScale + GeoM ebiten.GeoM +} + +var rectIndices = [6]uint16{0, 1, 2, 1, 2, 3} + +type point struct { + X, Y float32 +} + +func pt(x, y float64) point { + return point{ + X: float32(x), + Y: float32(y), + } +} + +type rectOpts struct { + Dsts [4]point + SrcX0, SrcY0 float32 + SrcX1, SrcY1 float32 + R, G, B, A float32 +} + +// adjustDestinationPixel is the original ebitengine implementation found here: +// https://github.com/hajimehoshi/ebiten/blob/v2.8.0-alpha.1/internal/graphics/vertex.go#L102-L126 +func adjustDestinationPixel(x float32) float32 { + // Avoid the center of the pixel, which is problematic (#929, #1171). + // Instead, align the vertices with about 1/3 pixels. + // + // The intention here is roughly this code: + // + // float32(math.Floor((float64(x)+1.0/6.0)*3) / 3) + // + // The actual implementation is more optimized than the above implementation. + ix := float32(int(x)) + if x < 0 && x != ix { + ix -= 1 + } + frac := x - ix + switch { + case frac < 3.0/16.0: + return ix + case frac < 8.0/16.0: + return ix + 5.0/16.0 + case frac < 13.0/16.0: + return ix + 11.0/16.0 + default: + return ix + 16.0/16.0 + } +} + +func appendRectVerticesIndices(vertices []ebiten.Vertex, indices []uint16, index int, opts *rectOpts) ([]ebiten.Vertex, []uint16) { + sx0, sy0, sx1, sy1 := opts.SrcX0, opts.SrcY0, opts.SrcX1, opts.SrcY1 + r, g, b, a := opts.R, opts.G, opts.B, opts.A + vertices = append(vertices, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[0].X), + DstY: adjustDestinationPixel(opts.Dsts[0].Y), + SrcX: sx0, + SrcY: sy0, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[1].X), + DstY: adjustDestinationPixel(opts.Dsts[1].Y), + SrcX: sx1, + SrcY: sy0, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[2].X), + DstY: adjustDestinationPixel(opts.Dsts[2].Y), + SrcX: sx0, + SrcY: sy1, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[3].X), + DstY: adjustDestinationPixel(opts.Dsts[3].Y), + SrcX: sx1, + SrcY: sy1, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ) + + indiceCursor := uint16(index * 4) + indices = append(indices, + rectIndices[0]+indiceCursor, + rectIndices[1]+indiceCursor, + rectIndices[2]+indiceCursor, + rectIndices[3]+indiceCursor, + rectIndices[4]+indiceCursor, + rectIndices[5]+indiceCursor, + ) + + return vertices, indices +} + +// Add adds DrawImage commands to the DrawList, images from multiple +// atlases can be added but they will break the previous batch bound to +// a different atlas, requiring an additional draw call internally. +// So, it is better to have the maximum of consecutive DrawCommand images +// sharing the same atlas. +func (dl *drawList) Add(commands ...*drawCommand) { + if len(commands) == 0 { + return + } + + var batch *drawRange + + if len(dl.ranges) > 0 { + batch = &dl.ranges[len(dl.ranges)-1] + } else { + dl.ranges = append(dl.ranges, drawRange{ + atlas: commands[0].Image.atlas, + }) + batch = &dl.ranges[0] + } + // Add vertices and indices + opts := &rectOpts{} + for _, cmd := range commands { + if cmd.Image.atlas != batch.atlas { + dl.ranges = append(dl.ranges, drawRange{ + atlas: cmd.Image.atlas, + }) + batch = &dl.ranges[len(dl.ranges)-1] + } + + // Dst attributes + bounds := cmd.Image.node.Region() + opts.Dsts[0] = pt(cmd.GeoM.Apply(0, 0)) + opts.Dsts[1] = pt(cmd.GeoM.Apply( + float64(bounds.Dx()), 0, + )) + opts.Dsts[2] = pt(cmd.GeoM.Apply( + 0, float64(bounds.Dy()), + )) + opts.Dsts[3] = pt(cmd.GeoM.Apply( + float64(bounds.Dx()), float64(bounds.Dy()), + )) + + // Color and source attributes + opts.R = cmd.ColorScale.R() + opts.G = cmd.ColorScale.G() + opts.B = cmd.ColorScale.B() + opts.A = cmd.ColorScale.A() + opts.SrcX0 = float32(bounds.Min.X) + opts.SrcY0 = float32(bounds.Min.Y) + opts.SrcX1 = float32(bounds.Max.X) + opts.SrcY1 = float32(bounds.Max.Y) + + dl.vx, dl.ix = appendRectVerticesIndices( + dl.vx, dl.ix, batch.end, opts, + ) + + batch.end++ + } +} + +// DrawOptions are additional options that will be applied to +// all draw commands from the draw list when calling Flush(). +type drawOptions struct { + ColorScaleMode ebiten.ColorScaleMode + Blend ebiten.Blend + Filter ebiten.Filter + Address ebiten.Address + AntiAlias bool +} + +// Flush executes all the draw commands as the smallest possible +// amount of draw calls, and then clears the list for next uses. +func (dl *drawList) Flush(dst *ebiten.Image, opts *drawOptions) { + var topts *ebiten.DrawTrianglesOptions + if opts != nil { + topts = &ebiten.DrawTrianglesOptions{ + ColorScaleMode: opts.ColorScaleMode, + Blend: opts.Blend, + Filter: opts.Filter, + Address: opts.Address, + AntiAlias: opts.AntiAlias, + } + } + index := 0 + for _, r := range dl.ranges { + dst.DrawTriangles( + dl.vx[index*4:(index+r.end)*4], + dl.ix[index*6:(index+r.end)*6], + r.atlas.image, + topts, + ) + index += r.end + } + // Clear buffers + dl.ranges = dl.ranges[:0] + dl.vx = dl.vx[:0] + dl.ix = dl.ix[:0] +} \ No newline at end of file diff --git a/text/v2/glyph.go b/text/v2/glyph.go index 9b08c345b..f32b0ce99 100644 --- a/text/v2/glyph.go +++ b/text/v2/glyph.go @@ -18,7 +18,6 @@ import ( "math" "sync" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/internal/hook" ) @@ -38,7 +37,7 @@ func init() { } type glyphImageCacheEntry struct { - image *atlas.Image + image *glyphImage atime int64 } @@ -49,7 +48,7 @@ type glyphImageCache[Key comparable] struct { m sync.Mutex } -func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { +func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *glyphImage) *glyphImage { g.m.Lock() defer g.m.Unlock() diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 9fb81869a..8dd971f93 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -19,7 +19,6 @@ import ( "encoding/binary" "fmt" - "github.com/Zyko0/Ebiary/atlas" "github.com/go-text/typesetting/di" glanguage "github.com/go-text/typesetting/language" "github.com/go-text/typesetting/opentype/api/font" @@ -333,7 +332,7 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse return glyphs } -func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*atlas.Image, int, int) { +func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*glyphImage, int, int) { if g.direction().isHorizontal() { origin.X = adjustGranularity(origin.X, g) origin.Y &^= ((1 << 6) - 1) @@ -353,7 +352,7 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*atlas.Ima yoffset: subpixelOffset.Y, variations: g.ensureVariationsString(), } - img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *atlas.Image { + img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *glyphImage { return segmentsToImage(a, glyph.scaledSegments, subpixelOffset, b) }) diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index 9c15789f0..002e34e26 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -19,7 +19,6 @@ import ( "io" "sync" - "github.com/Zyko0/Ebiary/atlas" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/language" "github.com/go-text/typesetting/opentype/api" @@ -281,7 +280,7 @@ func (g *GoTextFaceSource) scale(size float64) float64 { return size / float64(g.f.Upem()) } -func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { +func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *glyphImage) *glyphImage { if g.glyphImageCache == nil { g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{} } diff --git a/text/v2/gotextseg.go b/text/v2/gotextseg.go index 7579e9bc0..62605242c 100644 --- a/text/v2/gotextseg.go +++ b/text/v2/gotextseg.go @@ -19,7 +19,6 @@ import ( "image/draw" "math" - "github.com/Zyko0/Ebiary/atlas" gvector "golang.org/x/image/vector" "github.com/go-text/typesetting/opentype/api" @@ -76,7 +75,7 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 { } } -func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { +func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage { if len(segs) == 0 { return nil } diff --git a/text/v2/gox.go b/text/v2/gox.go index 19d7160a6..f8cefa228 100644 --- a/text/v2/gox.go +++ b/text/v2/gox.go @@ -21,7 +21,6 @@ import ( "golang.org/x/image/font" "golang.org/x/image/math/fixed" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -133,7 +132,7 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i return glyphs } -func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, int, fixed.Int26_6) { +func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*glyphImage, int, int, fixed.Int26_6) { // Assume that GoXFace's direction is always horizontal. origin.X = adjustGranularity(origin.X, s) origin.Y &^= ((1 << 6) - 1) @@ -147,7 +146,7 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, rune: r, xoffset: subpixelOffset.X, } - img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *atlas.Image { + img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *glyphImage { return s.glyphImageImpl(a, r, subpixelOffset, b) }) imgX := (origin.X + b.Min.X).Floor() @@ -155,7 +154,7 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, return img, imgX, imgY, a } -func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { +func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage { w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil() if w == 0 || h == 0 { return nil diff --git a/text/v2/layout.go b/text/v2/layout.go index 0dcad471a..6f0f8506b 100644 --- a/text/v2/layout.go +++ b/text/v2/layout.go @@ -17,7 +17,6 @@ package text import ( "strings" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -112,8 +111,8 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { geoM := drawOp.GeoM - dl := &atlas.DrawList{} - dc := &atlas.DrawCommand{} + dl := &drawList{} + dc := &drawCommand{} for _, g := range AppendGlyphs(nil, text, face, &layoutOp) { if g.Image == nil { continue @@ -125,7 +124,7 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { dc.Image = g.img dl.Add(dc) } - dl.Flush(dst, &atlas.DrawOptions{ + dl.Flush(dst, &drawOptions{ Blend: drawOp.Blend, Filter: drawOp.Filter, }) diff --git a/text/v2/text.go b/text/v2/text.go index 5c17cae69..e47d89d0f 100644 --- a/text/v2/text.go +++ b/text/v2/text.go @@ -22,7 +22,6 @@ import ( "golang.org/x/image/math/fixed" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -119,7 +118,7 @@ type Glyph struct { // Image is a rasterized glyph image. // Image is a grayscale image i.e. RGBA values are the same. // Image should be used as a render source and should not be modified. - img *atlas.Image + img *glyphImage // StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs. StartIndexInBytes int