diff --git a/main.go b/main.go index 8f9ad0e..3254f3b 100755 --- a/main.go +++ b/main.go @@ -1,10 +1,10 @@ package main import ( - "fmt" "image" "image/draw" "image/png" + "math" "os" "unicode" @@ -12,6 +12,7 @@ import ( "github.com/bloeys/nmage/input" "github.com/bloeys/nmage/renderer/rend3dgl" nmageimgui "github.com/bloeys/nmage/ui/imgui" + "github.com/bloeys/nterm/assert" "github.com/golang/freetype/truetype" "github.com/veandco/go-sdl2/sdl" "golang.org/x/image/font" @@ -56,36 +57,103 @@ func (p *program) Init() { panic("Failed to parse font. Err: " + err.Error()) } - size := 40 - face := truetype.NewFace(f, &truetype.Options{Size: float64(size), DPI: 72}) - imgFromText("Hello there my friend", size, face, "./text.png") - fmt.Println(string(getGlyphs(loadFontRanges(f)))) + pointSize := 40 + face := truetype.NewFace(f, &truetype.Options{Size: float64(pointSize), DPI: 72}) + genTextureAtlas(f, face, pointSize) } -func imgFromText(text string, textSize int, face font.Face, file string) { +type FontTexAtlas struct { + Img *image.RGBA + Glyphs map[rune]FontTexAtlasGlyph +} - //Create a white image - rgbaDest := image.NewRGBA(image.Rect(0, 0, 640, 480)) - draw.Draw(rgbaDest, rgbaDest.Bounds(), image.White, image.Point{}, draw.Src) +type FontTexAtlasGlyph struct { + U float32 + V float32 +} + +func genTextureAtlas(f *truetype.Font, face font.Face, textSize int) *FontTexAtlas { + + const maxAtlasSize = 8192 + + glyphs := getGlyphsFromRanges(getGlyphRanges(f)) + + assert.T(len(glyphs) > 0, "no glyphs") + + //Choose atlas size + atlasSizeX := 512 + atlasSizeY := 512 + + _, charWidthFixed, _ := face.GlyphBounds(glyphs[0]) + charWidth := charWidthFixed.Round() + lineHeight := face.Metrics().Height.Round() + + maxLinesInAtlas := atlasSizeY/lineHeight - 1 + charsPerLine := atlasSizeX / charWidth + linesNeeded := int(math.Ceil(float64(len(glyphs)) / float64(charsPerLine))) + + for linesNeeded > maxLinesInAtlas { + + atlasSizeX *= 2 + atlasSizeY *= 2 + + maxLinesInAtlas = atlasSizeY/lineHeight - 1 + + charsPerLine = atlasSizeX / charWidth + linesNeeded = int(math.Ceil(float64(len(glyphs)) / float64(charsPerLine))) + } + assert.T(atlasSizeX <= maxAtlasSize, "Atlas size went beyond maximum") + + //Create atlas + atlas := &FontTexAtlas{ + Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)), + Glyphs: make(map[rune]FontTexAtlasGlyph, len(glyphs)), + } + + //Clear img to white + draw.Draw(atlas.Img, atlas.Img.Bounds(), image.White, image.Point{}, draw.Src) - //Draw black text on image drawer := &font.Drawer{ - Dst: rgbaDest, + Dst: atlas.Img, Src: image.Black, Face: face, } - drawer.Dot = fixed.P(0, textSize) - drawer.DrawString(text) + //Put glyphs on atlas + charsOnLine := 0 + lineDx := fixed.P(0, lineHeight) + drawer.Dot = fixed.P(0, lineHeight) + for _, g := range glyphs { + + atlas.Glyphs[g] = FontTexAtlasGlyph{ + U: float32(drawer.Dot.X.Floor()) / float32(atlasSizeX), + V: (float32(atlasSizeY-drawer.Dot.Y.Floor()) / float32(atlasSizeY)), + } + drawer.DrawString(string(g)) + + charsOnLine++ + if charsOnLine == charsPerLine { + + charsOnLine = 0 + drawer.Dot.X = 0 + drawer.Dot = drawer.Dot.Add(lineDx) + } + } + + // fmt.Println(atlas.Glyphs) + saveImgToDisk(atlas.Img, "atlas.png") + return atlas +} + +func saveImgToDisk(img image.Image, file string) { - // Save that RGBA image to disk. outFile, err := os.Create(file) if err != nil { panic(err) } defer outFile.Close() - err = png.Encode(outFile, rgbaDest) + err = png.Encode(outFile, img) if err != nil { panic(err) } @@ -130,8 +198,8 @@ func (p *program) Deinit() { } -//loadFontRanges returns a list of ranges, each range is: [i][0]<=range<[i][1] -func loadFontRanges(f *truetype.Font) (ret [][2]rune) { +//getGlyphRanges returns a list of ranges, each range is: [i][0]<=range<[i][1] +func getGlyphRanges(f *truetype.Font) (ret [][2]rune) { rr := [2]rune{-1, -1} for r := rune(0); r <= unicode.MaxRune; r++ { if privateUseArea(r) { @@ -161,8 +229,8 @@ func privateUseArea(r rune) bool { 0x100000 <= r && r <= 0x10fffd } -//getGlyphs takes ranges of runes and produces an array of all the runes in these ranges -func getGlyphs(ranges [][2]rune) []rune { +//getGlyphsFromRanges takes ranges of runes and produces an array of all the runes in these ranges +func getGlyphsFromRanges(ranges [][2]rune) []rune { out := make([]rune, 0) for _, rr := range ranges {