mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
857 lines
26 KiB
Go
Executable File
857 lines
26 KiB
Go
Executable File
package glyphs
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"unicode"
|
|
|
|
"github.com/bloeys/gglm/gglm"
|
|
"github.com/bloeys/nmage/assets"
|
|
"github.com/bloeys/nmage/buffers"
|
|
"github.com/bloeys/nmage/camera"
|
|
"github.com/bloeys/nmage/materials"
|
|
"github.com/bloeys/nmage/meshes"
|
|
"github.com/bloeys/nterm/assert"
|
|
"github.com/bloeys/nterm/consts"
|
|
"github.com/go-gl/gl/v4.1-core/gl"
|
|
"github.com/golang/freetype/truetype"
|
|
)
|
|
|
|
const (
|
|
DefaultGlyphsPerBatch = 4 * 1024
|
|
|
|
floatsPerGlyph = 13
|
|
invalidRune = unicode.ReplacementChar
|
|
)
|
|
|
|
var (
|
|
RuneInfos map[rune]RuneInfo
|
|
)
|
|
|
|
type GlyphRendOpt uint64
|
|
|
|
const (
|
|
GlyphRendOpt_None GlyphRendOpt = 0
|
|
GlyphRendOpt_BgColor GlyphRendOpt = 1 << (iota - 1)
|
|
GlyphRendOpt_Underline
|
|
GlyphRendOpt_COUNT GlyphRendOpt = iota
|
|
)
|
|
|
|
type GlyphRendOptValues struct {
|
|
BgColor *gglm.Vec4
|
|
}
|
|
|
|
type GlyphRend struct {
|
|
Atlas *FontAtlas
|
|
AtlasTex *assets.Texture
|
|
|
|
GlyphMesh *meshes.Mesh
|
|
GlyphFgInstancedBuf buffers.Buffer
|
|
GlyphBgInstancedBuf buffers.Buffer
|
|
GlyphMat *materials.Material
|
|
TextRunsBuf []TextRun
|
|
|
|
//Luckily slices still work with go-opengl, so for now we will use our slice as an array (no appending)
|
|
GlyphFgCount uint32
|
|
GlyphFgVBO []float32
|
|
|
|
GlyphBgCount uint32
|
|
GlyphBgVBO []float32
|
|
|
|
ScreenWidth int32
|
|
ScreenHeight int32
|
|
|
|
SpacesPerTab uint
|
|
|
|
Opts GlyphRendOpt
|
|
OptValues GlyphRendOptValues
|
|
}
|
|
|
|
func (gr *GlyphRend) SetOpts(opts ...GlyphRendOpt) {
|
|
|
|
for _, v := range opts {
|
|
assert.T(v <= (1<<(GlyphRendOpt_COUNT-2)), "Invalid opts of value %d", opts)
|
|
|
|
if v == GlyphRendOpt_None {
|
|
gr.Opts = GlyphRendOpt_None
|
|
} else {
|
|
gr.Opts |= v
|
|
}
|
|
}
|
|
|
|
gl.ProgramUniform1ui(gr.GlyphMat.ShaderProg.ID, gr.GlyphMat.GetUnifLoc("opts1"), uint32(gr.Opts))
|
|
}
|
|
|
|
func (gr *GlyphRend) HasOpt(opt GlyphRendOpt) bool {
|
|
return gr.Opts&opt != 0
|
|
}
|
|
|
|
// DrawTextOpenGLAbs prepares text that will be drawn on the next GlyphRend.Draw call.
|
|
// screenPos is in the range [0,1], where (0,0) is the bottom left.
|
|
// Color is RGBA in the range [0,1].
|
|
func (gr *GlyphRend) DrawTextOpenGL01String(text string, screenPos *gglm.Vec3, color *gglm.Vec4) gglm.Vec3 {
|
|
screenPos.Set(screenPos.X()*float32(gr.ScreenWidth), screenPos.Y()*float32(gr.ScreenHeight), screenPos.Z())
|
|
return gr.DrawTextOpenGLAbsString(text, screenPos, color)
|
|
}
|
|
|
|
// DrawTextOpenGLAbsString prepares text that will be drawn on the next GlyphRend.Draw call.
|
|
// screenPos is in the range ([0,ScreenWidth],[0,ScreenHeight]) where (0,0) is bottom left.
|
|
// Color is RGBA in the range [0,1].
|
|
func (gr *GlyphRend) DrawTextOpenGLAbsString(text string, screenPos *gglm.Vec3, color *gglm.Vec4) gglm.Vec3 {
|
|
return gr.DrawTextOpenGLAbs([]rune(text), screenPos, color)
|
|
}
|
|
|
|
func (gr *GlyphRend) DrawTextOpenGL01(text []rune, screenPos *gglm.Vec3, color *gglm.Vec4) gglm.Vec3 {
|
|
screenPos.Set(screenPos.X()*float32(gr.ScreenWidth), screenPos.Y()*float32(gr.ScreenHeight), screenPos.Z())
|
|
return gr.DrawTextOpenGLAbs(text, screenPos, color)
|
|
}
|
|
|
|
// DrawTextOpenGLAbsString prepares text that will be drawn on the next GlyphRend.Draw call.
|
|
// screenPos is in the range ([0,ScreenWidth],[0,ScreenHeight]) where (0,0) is bottom left.
|
|
// Color is RGBA in the range [0,1].
|
|
func (gr *GlyphRend) DrawTextOpenGLAbs(text []rune, startPos *gglm.Vec3, color *gglm.Vec4) gglm.Vec3 {
|
|
|
|
runs := gr.TextRunsBuf[:]
|
|
gr.GetTextRuns(text, &runs)
|
|
if runs == nil {
|
|
return *startPos
|
|
}
|
|
|
|
drawPos := startPos.Clone()
|
|
lineHeightF32 := float32(gr.Atlas.LineHeight)
|
|
fgBufIndex, bgBufIndex := gr.getFgAndBgBufIndices()
|
|
for runIndex := 0; runIndex < len(runs); runIndex++ {
|
|
|
|
run := &runs[runIndex]
|
|
prevRune := invalidRune
|
|
|
|
screenWidthF32 := float32(gr.ScreenWidth)
|
|
if run.IsLtr {
|
|
|
|
for i := 0; i < len(run.Runes); i++ {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(startPos.X(), drawPos.Y()-lineHeightF32, startPos.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= screenWidthF32 {
|
|
|
|
drawPos.SetXYZ(startPos.X(), drawPos.Y()-lineHeightF32, startPos.Z())
|
|
// startPos.SetY(startPos.Y() - lineHeightF32)
|
|
// *drawPos = *startPos.Clone()
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
for i := len(run.Runes) - 1; i >= 0; i-- {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(startPos.X(), drawPos.Y()-lineHeightF32, startPos.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= screenWidthF32 {
|
|
drawPos.SetXYZ(startPos.X(), drawPos.Y()-lineHeightF32, startPos.Z())
|
|
// startPos.SetY(startPos.Y() - lineHeightF32)
|
|
// *drawPos = *startPos.Clone()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if consts.Mode_Debug && PrintPositions {
|
|
println("")
|
|
}
|
|
}
|
|
|
|
return *drawPos
|
|
}
|
|
|
|
func (gr *GlyphRend) DrawTextOpenGLAbsRect(text []rune, rectTopLeft *gglm.Vec3, rectBotRight *gglm.Vec2, color *gglm.Vec4) gglm.Vec3 {
|
|
|
|
runs := gr.TextRunsBuf[:]
|
|
gr.GetTextRuns(text, &runs)
|
|
if runs == nil {
|
|
return *rectTopLeft
|
|
}
|
|
|
|
drawPos := rectTopLeft.Clone()
|
|
lineHeightF32 := float32(gr.Atlas.LineHeight)
|
|
fgBufIndex, bgBufIndex := gr.getFgAndBgBufIndices()
|
|
for runIndex := 0; runIndex < len(runs); runIndex++ {
|
|
|
|
run := &runs[runIndex]
|
|
prevRune := invalidRune
|
|
|
|
if run.IsLtr {
|
|
|
|
for i := 0; i < len(run.Runes); i++ {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= rectBotRight.X() {
|
|
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
// rectTopLeft.SetY(rectTopLeft.Y() - lineHeightF32)
|
|
// *drawPos = *rectTopLeft.Clone()
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
for i := len(run.Runes) - 1; i >= 0; i-- {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= rectBotRight.X() {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
// rectTopLeft.SetY(rectTopLeft.Y() - lineHeightF32)
|
|
// *drawPos = *rectTopLeft.Clone()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if consts.Mode_Debug && PrintPositions {
|
|
println("")
|
|
}
|
|
}
|
|
|
|
return *drawPos
|
|
}
|
|
|
|
func (gr *GlyphRend) DrawTextOpenGLAbsRectWithStartPos(text []rune, startPos, rectTopLeft *gglm.Vec3, rectBotRight *gglm.Vec2, color *gglm.Vec4) gglm.Vec3 {
|
|
|
|
runs := gr.TextRunsBuf[:]
|
|
gr.GetTextRuns(text, &runs)
|
|
if runs == nil {
|
|
return *startPos
|
|
}
|
|
|
|
drawPos := startPos.Clone()
|
|
lineHeightF32 := float32(gr.Atlas.LineHeight)
|
|
fgBufIndex, bgBufIndex := gr.getFgAndBgBufIndices()
|
|
for runIndex := 0; runIndex < len(runs); runIndex++ {
|
|
|
|
run := &runs[runIndex]
|
|
prevRune := invalidRune
|
|
|
|
if run.IsLtr {
|
|
|
|
for i := 0; i < len(run.Runes); i++ {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= rectBotRight.X() {
|
|
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
// rectTopLeft.SetY(rectTopLeft.Y() - lineHeightF32)
|
|
// *drawPos = *rectTopLeft.Clone()
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
for i := len(run.Runes) - 1; i >= 0; i-- {
|
|
|
|
if run.Runes[i] == '\n' {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
}
|
|
|
|
gr.drawRune(run, i, prevRune, drawPos, color, lineHeightF32, &fgBufIndex, &bgBufIndex)
|
|
prevRune = run.Runes[i]
|
|
|
|
//Wrap
|
|
if drawPos.X()+gr.Atlas.SpaceAdvance >= rectBotRight.X() {
|
|
drawPos.SetXYZ(rectTopLeft.X(), drawPos.Y()-lineHeightF32, rectTopLeft.Z())
|
|
// rectTopLeft.SetY(rectTopLeft.Y() - lineHeightF32)
|
|
// *drawPos = *rectTopLeft.Clone()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if consts.Mode_Debug && PrintPositions {
|
|
println("")
|
|
}
|
|
}
|
|
|
|
return *drawPos
|
|
}
|
|
|
|
func (gr *GlyphRend) getFgAndBgBufIndices() (fgBufIndex, bgBufIndex uint32) {
|
|
return gr.GlyphFgCount * floatsPerGlyph, gr.GlyphBgCount * floatsPerGlyph
|
|
}
|
|
|
|
// @Debug
|
|
var PrintPositions bool
|
|
|
|
func (gr *GlyphRend) GridSize() (w, h int64) {
|
|
w = int64(gr.ScreenWidth) / int64(gr.Atlas.SpaceAdvance)
|
|
h = int64(gr.ScreenHeight) / int64(gr.Atlas.LineHeight)
|
|
return w, h
|
|
}
|
|
|
|
func (gr *GlyphRend) ScreenPosToGridPos(x, y float32) (gridX, gridY float32) {
|
|
gridX = floorF32(x / gr.Atlas.SpaceAdvance)
|
|
gridY = floorF32(y / gr.Atlas.LineHeight)
|
|
return gridX, gridY
|
|
}
|
|
|
|
func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, pos *gglm.Vec3, color *gglm.Vec4, lineHeightF32 float32, glyphFgBufIndex, glyphBgBufIndex *uint32) {
|
|
|
|
r := run.Runes[i]
|
|
if r == '\t' {
|
|
pos.AddX(gr.Atlas.SpaceAdvance * float32(gr.SpacesPerTab))
|
|
return
|
|
}
|
|
|
|
var g FontAtlasGlyph
|
|
if run.IsLtr {
|
|
if i < len(run.Runes)-1 {
|
|
//start or middle of sentence
|
|
g = GlyphFromRunes(gr.Atlas.Glyphs, r, prevRune, run.Runes[i+1])
|
|
} else {
|
|
//Last character
|
|
g = GlyphFromRunes(gr.Atlas.Glyphs, r, prevRune, invalidRune)
|
|
}
|
|
} else {
|
|
if i > 0 {
|
|
//start or middle of sentence
|
|
g = GlyphFromRunes(gr.Atlas.Glyphs, r, run.Runes[i-1], prevRune)
|
|
} else {
|
|
//Last character
|
|
g = GlyphFromRunes(gr.Atlas.Glyphs, r, invalidRune, prevRune)
|
|
}
|
|
}
|
|
|
|
//We must adjust char positioning according to: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
|
|
drawPos := *pos
|
|
//The flooring to an integer pixel must happen AFTER the (potentially) fractional adjustments have been made.
|
|
//This is what the truetype face.Rasterizer does and seems to give good results. Do NOT floor bearing/descent first.
|
|
drawPos.SetX(floorF32(drawPos.X() + g.BearingX))
|
|
drawPos.SetY(floorF32(drawPos.Y() - g.Descent))
|
|
|
|
if consts.Mode_Debug && PrintPositions {
|
|
oldXY := gglm.NewVec2(pos.X(), pos.Y())
|
|
newXY := gglm.NewVec2(drawPos.X(), drawPos.Y())
|
|
fmt.Printf("char=%s; PosBefore=%s, PosAfter=%s; Bearing/Decent=(%f, %f)\n", string(r), oldXY.String(), newXY.String(), g.BearingX, g.Descent)
|
|
}
|
|
|
|
//Add the glyph information to the vbo
|
|
if gr.HasOpt(GlyphRendOpt_BgColor) {
|
|
// UV
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+0] = -1
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+1] = -1
|
|
*glyphBgBufIndex += 2
|
|
|
|
//UVSize
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+0] = 0
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+1] = 0
|
|
*glyphBgBufIndex += 2
|
|
|
|
//Color
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+0] = gr.OptValues.BgColor.R()
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+1] = gr.OptValues.BgColor.G()
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+2] = gr.OptValues.BgColor.B()
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+3] = gr.OptValues.BgColor.A()
|
|
*glyphBgBufIndex += 4
|
|
|
|
//Model Pos
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+0] = pos.X()
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+1] = pos.Y()
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+2] = pos.Z()
|
|
*glyphBgBufIndex += 3
|
|
|
|
//Model Scale
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+0] = gr.Atlas.SpaceAdvance
|
|
gr.GlyphBgVBO[*glyphBgBufIndex+1] = lineHeightF32
|
|
*glyphBgBufIndex += 2
|
|
|
|
gr.GlyphBgCount++
|
|
if gr.GlyphBgCount == DefaultGlyphsPerBatch {
|
|
gr.Draw()
|
|
*glyphBgBufIndex = 0
|
|
}
|
|
}
|
|
|
|
//UV
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+0] = g.U
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+1] = g.V
|
|
*glyphFgBufIndex += 2
|
|
|
|
//UVSize
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+0] = g.SizeU
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+1] = g.SizeV
|
|
*glyphFgBufIndex += 2
|
|
|
|
//Color
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+0] = color.R()
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+1] = color.G()
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+2] = color.B()
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+3] = color.A()
|
|
*glyphFgBufIndex += 4
|
|
|
|
//Model Pos
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+0] = drawPos.X()
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+1] = drawPos.Y()
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+2] = drawPos.Z()
|
|
*glyphFgBufIndex += 3
|
|
|
|
//Model Scale
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+0] = g.SizeU
|
|
gr.GlyphFgVBO[*glyphFgBufIndex+1] = g.SizeV
|
|
*glyphFgBufIndex += 2
|
|
|
|
pos.AddX(g.Advance)
|
|
|
|
//If we fill the buffer we issue a draw call
|
|
gr.GlyphFgCount++
|
|
if gr.GlyphFgCount == DefaultGlyphsPerBatch {
|
|
gr.Draw()
|
|
*glyphFgBufIndex = 0
|
|
}
|
|
}
|
|
|
|
// func roundF32(x float32) float32 {
|
|
// return float32(math.Round(float64(x)))
|
|
// }
|
|
|
|
// func ceilF32(x float32) float32 {
|
|
// return float32(math.Ceil(float64(x)))
|
|
// }
|
|
|
|
func floorF32(x float32) float32 {
|
|
return float32(math.Floor(float64(x)))
|
|
}
|
|
|
|
type TextRun struct {
|
|
Runes []rune
|
|
IsLtr bool
|
|
}
|
|
|
|
func (gr *GlyphRend) GetTextRuns(rs []rune, textRunsBuf *[]TextRun) {
|
|
|
|
if len(rs) == 0 {
|
|
return
|
|
}
|
|
|
|
runs := textRunsBuf
|
|
currRunScript := RuneInfos[rs[0]].ScriptTable
|
|
|
|
//TODO: We need to detect neutral characters through BiDi category, not being in common
|
|
//TODO: Diacritics go into things like 'Category_Mn' and don't necessairly follow the parent script (e.g. Arabic diacritics are NOT in unicode.Arabic).
|
|
//They should be part of the same run but right now we split them into their own run.
|
|
runStartIndex := 0
|
|
for i := 1; i < len(rs); i++ {
|
|
|
|
r := rs[i]
|
|
ri := RuneInfos[r]
|
|
//A run is a set of characters using the same script (and other metrics) minus leading/trailing neutral characters
|
|
if ri.ScriptTable == currRunScript || ri.ScriptTable == unicode.Common {
|
|
continue
|
|
}
|
|
|
|
//We reached a new run so count trailing neutrals to be removed from this run
|
|
newRunRunes := rs[runStartIndex:i]
|
|
trailingCommonsCount := 0
|
|
for j := len(newRunRunes) - 1; j >= 0; j-- {
|
|
if !unicode.Is(unicode.Common, newRunRunes[j]) {
|
|
break
|
|
}
|
|
trailingCommonsCount++
|
|
}
|
|
|
|
//If we have a run without trailing neutrals or had a run of just neutrals (e.g. starting sentence with spaces)
|
|
//then the full run is added, otherwise we slice the run to put neturals in a separate run
|
|
if trailingCommonsCount == 0 || len(newRunRunes) == trailingCommonsCount {
|
|
*runs = append(*runs, TextRun{Runes: newRunRunes})
|
|
} else {
|
|
*runs = append(*runs,
|
|
TextRun{Runes: newRunRunes[:len(newRunRunes)-trailingCommonsCount]}, TextRun{Runes: newRunRunes[len(newRunRunes)-trailingCommonsCount:]})
|
|
}
|
|
|
|
//The removed neutrals are included as the start of the new run
|
|
runStartIndex = i
|
|
currRunScript = ri.ScriptTable
|
|
}
|
|
|
|
*runs = append(*runs, TextRun{Runes: rs[runStartIndex:]})
|
|
|
|
//Detect directionality of each run
|
|
for i := 0; i < len(*runs); i++ {
|
|
|
|
run := &(*runs)[i]
|
|
bidiCat := BidiCategory_L
|
|
for _, r := range run.Runes {
|
|
if !unicode.Is(unicode.Common, r) {
|
|
bidiCat = RuneInfos[r].BidiCat
|
|
break
|
|
}
|
|
}
|
|
run.IsLtr = !(bidiCat == BidiCategory_R || bidiCat == BidiCategory_AL || bidiCat == BidiCategory_RLE || bidiCat == BidiCategory_RLO || bidiCat == BidiCategory_RLI || bidiCat == BidiCategory_RLM)
|
|
}
|
|
}
|
|
|
|
// GlyphFromRunes does shaping where it selects the proper rune based (e.g. end Alef) on the surrounding runes
|
|
func GlyphFromRunes(glyphTable map[rune]FontAtlasGlyph, curr, prev, next rune) FontAtlasGlyph {
|
|
|
|
//PERF: Map access times are absolute garbage to the point that ~85%+ of the runtime of this func
|
|
//is spent reading from maps :). Using nSet or fMap or similar would be a lot better.
|
|
type PosCtx int
|
|
const (
|
|
PosCtx_start PosCtx = iota
|
|
PosCtx_mid
|
|
PosCtx_end
|
|
PosCtx_isolated
|
|
)
|
|
|
|
prevIsValid := prev != invalidRune
|
|
nextIsValid := next != invalidRune
|
|
|
|
//Isolated case
|
|
if !prevIsValid && !nextIsValid {
|
|
return glyphTable[curr]
|
|
}
|
|
|
|
ri := RuneInfos[curr]
|
|
if ri.JoinType == JoiningType_None || ri.JoinType == JoiningType_Transparent {
|
|
return glyphTable[curr]
|
|
}
|
|
|
|
prevJoinType := RuneInfos[prev].JoinType
|
|
joinWithRight := prevIsValid &&
|
|
(prevJoinType == JoiningType_Dual || prevJoinType == JoiningType_Left || prevJoinType == JoiningType_Causing) &&
|
|
(ri.JoinType == JoiningType_Dual || ri.JoinType == JoiningType_Right)
|
|
|
|
nextJoinType := RuneInfos[next].JoinType
|
|
joinWithLeft := nextIsValid &&
|
|
(nextJoinType == JoiningType_Dual || nextJoinType == JoiningType_Right || nextJoinType == JoiningType_Causing) &&
|
|
(ri.JoinType == JoiningType_Dual || ri.JoinType == JoiningType_Left)
|
|
|
|
var ctx PosCtx
|
|
if joinWithRight && joinWithLeft {
|
|
ctx = PosCtx_mid
|
|
} else if joinWithLeft {
|
|
ctx = PosCtx_start
|
|
} else if joinWithRight {
|
|
ctx = PosCtx_end
|
|
} else {
|
|
ctx = PosCtx_isolated
|
|
}
|
|
|
|
//This is only needed for Arabic (I think)
|
|
switch ctx {
|
|
case PosCtx_start:
|
|
|
|
for i := 0; i < len(ri.EquivalentRunes); i++ {
|
|
|
|
otherRune := ri.EquivalentRunes[i]
|
|
otherDecompTag := RuneInfos[otherRune].DecompTag
|
|
if otherDecompTag == DecompTag_initial {
|
|
curr = otherRune
|
|
break
|
|
}
|
|
}
|
|
|
|
case PosCtx_mid:
|
|
|
|
for i := 0; i < len(ri.EquivalentRunes); i++ {
|
|
|
|
otherRune := ri.EquivalentRunes[i]
|
|
otherDecompTag := RuneInfos[otherRune].DecompTag
|
|
if otherDecompTag == DecompTag_medial {
|
|
curr = otherRune
|
|
break
|
|
}
|
|
}
|
|
|
|
case PosCtx_end:
|
|
|
|
for i := 0; i < len(ri.EquivalentRunes); i++ {
|
|
|
|
otherRune := ri.EquivalentRunes[i]
|
|
otherDecompTag := RuneInfos[otherRune].DecompTag
|
|
if otherDecompTag == DecompTag_final {
|
|
curr = otherRune
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return glyphTable[curr]
|
|
}
|
|
|
|
func (gr *GlyphRend) Draw() {
|
|
|
|
if gr.GlyphFgCount == 0 && gr.GlyphBgCount == 0 {
|
|
return
|
|
}
|
|
|
|
// Set common GPU settings for both Fg and Bg
|
|
gr.GlyphMat.Bind()
|
|
gl.Disable(gl.DEPTH_TEST) //We need to disable depth testing so that nearby characters don't occlude each other
|
|
|
|
// We must draw Bg BEFORE foreground because they write to the same pixels.
|
|
// Drawing everything in one instance buffer/batch doesn't work because the order in which instances are drawn in a batch is not guaranteed,
|
|
// which means a background might be drawn on top of a foreground glyph.
|
|
if gr.GlyphBgCount > 0 {
|
|
|
|
gl.BindVertexArray(gr.GlyphBgInstancedBuf.VAOID)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, gr.GlyphBgInstancedBuf.BufID)
|
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, int(gr.GlyphBgCount*floatsPerGlyph)*4, gl.Ptr(&gr.GlyphBgVBO[:gr.GlyphBgCount*floatsPerGlyph][0]))
|
|
|
|
gl.DrawElementsInstanced(gl.TRIANGLES, gr.GlyphBgInstancedBuf.IndexBufCount, gl.UNSIGNED_INT, gl.PtrOffset(0), int32(gr.GlyphBgCount))
|
|
gr.GlyphBgCount = 0
|
|
}
|
|
|
|
if gr.GlyphFgCount > 0 {
|
|
gl.BindVertexArray(gr.GlyphFgInstancedBuf.VAOID)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, gr.GlyphFgInstancedBuf.BufID)
|
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, int(gr.GlyphFgCount*floatsPerGlyph)*4, gl.Ptr(&gr.GlyphFgVBO[:gr.GlyphFgCount*floatsPerGlyph][0]))
|
|
|
|
gl.DrawElementsInstanced(gl.TRIANGLES, gr.GlyphFgInstancedBuf.IndexBufCount, gl.UNSIGNED_INT, gl.PtrOffset(0), int32(gr.GlyphFgCount))
|
|
gr.GlyphFgCount = 0
|
|
}
|
|
|
|
gl.Enable(gl.DEPTH_TEST)
|
|
}
|
|
|
|
// SetFace updates the underlying font atlas used by the glyph renderer.
|
|
// The current atlas is unchanged if there is an error
|
|
func (gr *GlyphRend) SetFace(fontOptions *truetype.Options) error {
|
|
|
|
face := truetype.NewFace(gr.Atlas.Font, fontOptions)
|
|
newAtlas, err := NewFontAtlasFromFont(gr.Atlas.Font, face, uint(fontOptions.Size))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gr.Atlas = newAtlas
|
|
gr.updateFontAtlasTexture()
|
|
return nil
|
|
}
|
|
|
|
func (gr *GlyphRend) SetFontFromFile(fontFile string, fontOptions *truetype.Options) error {
|
|
|
|
atlas, err := NewFontAtlasFromFile(fontFile, fontOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gr.Atlas = atlas
|
|
gr.updateFontAtlasTexture()
|
|
return nil
|
|
}
|
|
|
|
// updateFontAtlasTexture uploads the texture representing the font atlas to the GPU
|
|
// and updates the GlyphRend.AtlasTex field.
|
|
//
|
|
// Any old textures are deleted
|
|
func (gr *GlyphRend) updateFontAtlasTexture() error {
|
|
|
|
//Clean old texture and load new texture
|
|
if gr.AtlasTex != nil {
|
|
gl.DeleteTextures(1, &gr.AtlasTex.TexID)
|
|
gr.AtlasTex = nil
|
|
}
|
|
|
|
atlasTex, err := assets.LoadTextureInMemPngImg(gr.Atlas.Img, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gr.AtlasTex = &atlasTex
|
|
|
|
gl.BindTexture(gl.TEXTURE_2D, atlasTex.TexID)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
|
gl.BindTexture(gl.TEXTURE_2D, 0)
|
|
|
|
//Update material
|
|
gr.GlyphMat.DiffuseTex = gr.AtlasTex.TexID
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gr *GlyphRend) SetScreenSize(screenWidth, screenHeight int32) {
|
|
|
|
gr.ScreenWidth = screenWidth
|
|
gr.ScreenHeight = screenHeight
|
|
|
|
//The camera is orthographic and fits the screen size exactly. This is needed so we can size and position characters correctly.
|
|
cam := camera.NewOrthographic(gglm.NewVec3(0, 0, 10), gglm.NewVec3(0, 0, -1), gglm.NewVec3(0, 1, 0), 0.1, 20, 0, float32(screenWidth), float32(screenHeight), 0)
|
|
projViewMtx := cam.ProjMat.Mul(&cam.ViewMat)
|
|
|
|
gr.GlyphMat.SetUnifMat4("projViewMat", projViewMtx)
|
|
}
|
|
|
|
func NewGlyphRend(fontFile string, fontOptions *truetype.Options, screenWidth, screenHeight int32) (*GlyphRend, error) {
|
|
|
|
var err error
|
|
if RuneInfos == nil {
|
|
RuneInfos, err = ParseUnicodeData("./unicode-data-13.txt", "./arabic-shaping-13.txt")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
gr := &GlyphRend{
|
|
GlyphFgCount: 0,
|
|
GlyphFgVBO: make([]float32, floatsPerGlyph*DefaultGlyphsPerBatch),
|
|
|
|
GlyphBgCount: 0,
|
|
GlyphBgVBO: make([]float32, floatsPerGlyph*DefaultGlyphsPerBatch),
|
|
TextRunsBuf: make([]TextRun, 0, 20),
|
|
SpacesPerTab: 4,
|
|
|
|
Opts: GlyphRendOpt_None,
|
|
OptValues: GlyphRendOptValues{
|
|
BgColor: gglm.NewVec4(0, 0, 0, 0),
|
|
},
|
|
}
|
|
|
|
//Create glyph mesh
|
|
gr.GlyphMesh = &meshes.Mesh{
|
|
Name: "glypQuad",
|
|
|
|
//VertPos only. Instanced attributes are stored separately
|
|
Buf: buffers.NewBuffer(
|
|
buffers.Element{ElementType: buffers.DataTypeVec3},
|
|
),
|
|
}
|
|
|
|
//The quad must be anchored at the bottom-left, not it's center (i.e. bottom-left vertex must be at 0,0)
|
|
gr.GlyphMesh.Buf.SetData([]float32{
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
1, 1, 0,
|
|
})
|
|
|
|
gr.GlyphMesh.Buf.SetIndexBufData([]uint32{
|
|
0, 1, 2,
|
|
1, 3, 2,
|
|
})
|
|
|
|
//Setup material
|
|
gr.GlyphMat = materials.NewMaterial("glyphMat", "./res/shaders/glyph.glsl")
|
|
|
|
//With the material ready we can generate the atlas
|
|
gr.Atlas, err = NewFontAtlasFromFile(fontFile, fontOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = gr.updateFontAtlasTexture()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// In each of these buffers we use one the mesh VBO for vertex position data (vertex attrib 0), and the other VBO created here for the rest.
|
|
// We split bg/fg into separate buffers and draw calls to ensure bg is drawn first, because order of drawing instanced items sent in the same
|
|
// call is not guaranteed
|
|
gr.GlyphFgInstancedBuf = buffers.NewBuffer()
|
|
gr.GlyphBgInstancedBuf = buffers.NewBuffer()
|
|
|
|
// Create instanced buffers and set their vertex attributes.
|
|
glyphMeshVertPosLayoutEle := gr.GlyphMesh.Buf.GetLayout()[0]
|
|
setupGlyphInstanceBuf := func(instanceBuf *buffers.Buffer, vbo *[]float32) {
|
|
|
|
instanceBuf.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
|
|
)
|
|
|
|
// Use the index buffer based on the mesh data
|
|
instanceBuf.IndexBufID = gr.GlyphMesh.Buf.IndexBufID
|
|
instanceBuf.SetIndexBufData([]uint32{
|
|
0, 1, 2,
|
|
1, 3, 2,
|
|
})
|
|
|
|
// Set index buf data unbinds at the end so we re-bind here
|
|
instanceBuf.Bind()
|
|
|
|
// Set vertex attribute zero to use the buffer of the glyph mesh, which contains onlt vertex positions.
|
|
// Since all instances have the same vertex positions we only send this data in the first vertex and all instances refer
|
|
// to the position information of the first vertex. This is done by setting an attrib divisor of zero for this attribute
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, gr.GlyphMesh.Buf.BufID)
|
|
gl.EnableVertexAttribArray(0)
|
|
gl.VertexAttribPointer(0, glyphMeshVertPosLayoutEle.ElementType.CompCount(), glyphMeshVertPosLayoutEle.ElementType.GLType(), false, gr.GlyphMesh.Buf.Stride, gl.PtrOffset(glyphMeshVertPosLayoutEle.Offset))
|
|
gl.VertexAttribDivisor(0, 0)
|
|
|
|
// Set the rest of the vertex attributes to use the instance buffer
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, instanceBuf.BufID)
|
|
layout := instanceBuf.GetLayout()
|
|
|
|
uvEle := layout[0]
|
|
gl.EnableVertexAttribArray(1)
|
|
gl.VertexAttribPointer(1, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, instanceBuf.Stride, gl.PtrOffset(uvEle.Offset))
|
|
gl.VertexAttribDivisor(1, 1)
|
|
|
|
uvSize := layout[1]
|
|
gl.EnableVertexAttribArray(2)
|
|
gl.VertexAttribPointer(2, uvSize.ElementType.CompCount(), uvSize.ElementType.GLType(), false, instanceBuf.Stride, gl.PtrOffset(uvSize.Offset))
|
|
gl.VertexAttribDivisor(2, 1)
|
|
|
|
colorEle := layout[2]
|
|
gl.EnableVertexAttribArray(3)
|
|
gl.VertexAttribPointer(3, colorEle.ElementType.CompCount(), colorEle.ElementType.GLType(), false, instanceBuf.Stride, gl.PtrOffset(colorEle.Offset))
|
|
gl.VertexAttribDivisor(3, 1)
|
|
|
|
posEle := layout[3]
|
|
gl.EnableVertexAttribArray(4)
|
|
gl.VertexAttribPointer(4, posEle.ElementType.CompCount(), posEle.ElementType.GLType(), false, instanceBuf.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, instanceBuf.Stride, gl.PtrOffset(scaleEle.Offset))
|
|
gl.VertexAttribDivisor(5, 1)
|
|
|
|
//Fill buffer with zeros and set to dynamic so in the actual draw calls we use bufferSubData which makes things a lot faster
|
|
gl.BufferData(gl.ARRAY_BUFFER, len(*vbo)*4, gl.Ptr(&(*vbo)[0]), buffers.BufUsage_Dynamic.ToGL())
|
|
|
|
//Reset mesh layout because the instancedBuf setLayout over-wrote vertex attribute 0
|
|
gr.GlyphMesh.Buf.SetLayout(buffers.Element{ElementType: buffers.DataTypeVec3})
|
|
}
|
|
|
|
setupGlyphInstanceBuf(&gr.GlyphFgInstancedBuf, &gr.GlyphFgVBO)
|
|
setupGlyphInstanceBuf(&gr.GlyphBgInstancedBuf, &gr.GlyphBgVBO)
|
|
|
|
gr.SetScreenSize(screenWidth, screenHeight)
|
|
|
|
return gr, nil
|
|
}
|