Abstract glyph renderer

This commit is contained in:
bloeys
2022-07-01 10:20:30 +04:00
parent aa9aa8347c
commit 224f33bf9f
3 changed files with 477 additions and 491 deletions

297
glyphs/glyphs.go Executable file
View File

@ -0,0 +1,297 @@
package glyphs
import (
"math/rand"
"os"
"unicode"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assets"
"github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/golang/freetype/truetype"
)
type GlyphRend struct {
Atlas *FontAtlas
AtlasTex *assets.Texture
GlyphMesh *meshes.Mesh
InstancedBuf buffers.Buffer
GlyphMat *materials.Material
GlyphCount int32
GlyphVBO []float32
}
//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) {
continue
}
if f.Index(r) == 0 {
continue
}
if rr[1] == r {
rr[1] = r + 1
continue
}
if rr[0] != -1 {
ret = append(ret, rr)
}
rr = [2]rune{r, r + 1}
}
if rr[0] != -1 {
ret = append(ret, rr)
}
return ret
}
func privateUseArea(r rune) bool {
return 0xe000 <= r && r <= 0xf8ff ||
0xf0000 <= r && r <= 0xffffd ||
0x100000 <= r && r <= 0x10fffd
}
//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 {
temp := make([]rune, 0, rr[1]-rr[0])
for r := rr[0]; r < rr[1]; r++ {
temp = append(temp, r)
}
out = append(out, temp...)
}
return out
}
func (gr *GlyphRend) DrawTextOpenGL(text string, startPos *gglm.Vec3, winWidth, winHeight int32) {
//The projection matrix fits the screen size. This is needed so we can size and position characters correctly.
projMtx := gglm.Ortho(0, float32(winWidth), float32(winHeight), 0, 0.1, 10)
viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -1), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0))
projViewMtx := projMtx.Clone().Mul(viewMtx)
gr.GlyphMat.DiffuseTex = gr.AtlasTex.TexID
gr.GlyphMat.SetUnifMat4("projViewMat", &projViewMtx.Mat4)
gr.GlyphMat.Bind()
//Prepass to pre-allocate the buffer
rs := []rune(text)
const floatsPerGlyph = 18
rCol := rand.Float32()
gCol := rand.Float32()
bCol := rand.Float32()
pos := startPos.Clone()
instancedData := make([]float32, 0, len(rs)*floatsPerGlyph) //This a larger approximation than needed because we don't count spaces etc
for i := 0; i < len(rs); i++ {
r := rs[i]
g := gr.Atlas.Glyphs[r]
if r == '\n' {
startPos.SetY(startPos.Y() - float32(gr.Atlas.LineHeight))
pos = startPos.Clone()
continue
}
gr.GlyphCount++
height := float32(g.Ascent + g.Descent)
scale := gglm.NewVec3(g.Advance, height, 1)
//See: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
//Quads are drawn from the center and so that's our baseline. But chars shouldn't be centered, they should follow ascent/decent/advance.
//To make them do that vertically, we raise them above the baseline (y+height/2), then since they are sitting on top of the baseline we can simply
//move them down by the decent amount to put them in the correct vertical position.
//
//Horizontally the character should be drawn from the left edge not the center, so we just move it forward by advance/2
drawPos := *pos
drawPos.SetX(drawPos.X() + g.Advance*0.5)
drawPos.SetY(drawPos.Y() + height*0.5 - g.Descent)
instancedData = append(instancedData, []float32{
g.U, g.V,
g.U + g.SizeU, g.V,
g.U, g.V + g.SizeV,
g.U + g.SizeU, g.V + g.SizeV,
rCol, gCol, bCol, 1, //Color
drawPos.X(), drawPos.Y(), drawPos.Z(), //Model pos
scale.X(), scale.Y(), scale.Z(), //Model scale
}...)
pos.SetX(pos.X() + g.Advance)
}
gr.GlyphVBO = append(gr.GlyphVBO, instancedData...)
}
func (gr *GlyphRend) Draw() {
gr.InstancedBuf.SetData(gr.GlyphVBO)
gr.InstancedBuf.Bind()
gr.GlyphMat.Bind()
gl.DrawElementsInstanced(gl.TRIANGLES, gr.GlyphMesh.Buf.IndexBufCount, gl.UNSIGNED_INT, gl.PtrOffset(0), gr.GlyphCount)
gr.InstancedBuf.UnBind()
gr.GlyphCount = 0
gr.GlyphVBO = []float32{}
}
func (gr *GlyphRend) SetFace(fontOptions *truetype.Options) {
face := truetype.NewFace(gr.Atlas.Font, fontOptions)
gr.Atlas = NewFontAtlasFromFont(gr.Atlas.Font, face, uint(fontOptions.Size))
}
func (gr *GlyphRend) SetFontFromFile(fontFile string, fontOptions *truetype.Options) error {
atlas, err := NewFontAtlasFromFile(fontFile, fontOptions)
if err != nil {
return err
}
gr.Atlas = atlas
return nil
}
func NewGlyphRend(fontFile string, fontOptions *truetype.Options) (*GlyphRend, error) {
atlas, err := NewFontAtlasFromFile(fontFile, fontOptions)
if err != nil {
return nil, err
}
//Create OpenGL texture
pngFileName := fontFile + "atlas.png"
err = SaveImgToPNG(atlas.Img, pngFileName)
if err != nil {
return nil, err
}
atlasTex, err := assets.LoadPNGTexture(pngFileName)
if err != nil {
return nil, err
}
os.Remove(pngFileName)
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.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.BindTexture(gl.TEXTURE_2D, 0)
//Create glyph mesh
glyphMesh := &meshes.Mesh{
Name: "glypQuad",
//VertPos, UV, Color; Instanced attributes are stored separately
Buf: buffers.NewBuffer(
buffers.Element{ElementType: buffers.DataTypeVec3},
),
}
glyphMesh.Buf.SetData([]float32{
-0.5, -0.5, 0,
0.5, -0.5, 0,
-0.5, 0.5, 0,
0.5, 0.5, 0,
})
glyphMesh.Buf.SetIndexBufData([]uint32{
0, 1, 2,
1, 3, 2,
})
//Setup material
glyphMat := materials.NewMaterial("glyphMat", "./res/shaders/glyph")
glyphMat.SetAttribute(glyphMesh.Buf)
//Create instanced buf and set its instanced attributes.
//Multiple VBOs under one VAO, one VBO for vertex data, and one VBO for instanced data.
instancedBuf := buffers.Buffer{
VAOID: glyphMesh.Buf.VAOID,
}
instancedBuf.SetLayout(
buffers.Element{ElementType: buffers.DataTypeVec2}, //UVST0
buffers.Element{ElementType: buffers.DataTypeVec2}, //UVST1
buffers.Element{ElementType: buffers.DataTypeVec2}, //UVST2
buffers.Element{ElementType: buffers.DataTypeVec2}, //UVST3
buffers.Element{ElementType: buffers.DataTypeVec4}, //Color
buffers.Element{ElementType: buffers.DataTypeVec3}, //ModelPos
buffers.Element{ElementType: buffers.DataTypeVec3}, //ModelScale
)
gl.GenBuffers(1, &instancedBuf.BufID)
if instancedBuf.BufID == 0 {
panic("Failed to create openGL buffer")
}
instancedBuf.Bind()
gl.BindBuffer(gl.ARRAY_BUFFER, instancedBuf.BufID)
layout := instancedBuf.GetLayout()
//4 UV values
uvEle := layout[0]
gl.EnableVertexAttribArray(1)
gl.VertexAttribPointer(1, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(uvEle.Offset))
gl.VertexAttribDivisor(1, 1)
uvEle = layout[1]
gl.EnableVertexAttribArray(2)
gl.VertexAttribPointer(2, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(uvEle.Offset))
gl.VertexAttribDivisor(2, 1)
uvEle = layout[2]
gl.EnableVertexAttribArray(3)
gl.VertexAttribPointer(3, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(uvEle.Offset))
gl.VertexAttribDivisor(3, 1)
uvEle = layout[3]
gl.EnableVertexAttribArray(4)
gl.VertexAttribPointer(4, uvEle.ElementType.CompCount(), uvEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(uvEle.Offset))
gl.VertexAttribDivisor(4, 1)
//Rest of instanced attributes
colorEle := layout[4]
gl.EnableVertexAttribArray(5)
gl.VertexAttribPointer(5, colorEle.ElementType.CompCount(), colorEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(colorEle.Offset))
gl.VertexAttribDivisor(5, 1)
posEle := layout[5]
gl.EnableVertexAttribArray(6)
gl.VertexAttribPointer(6, posEle.ElementType.CompCount(), posEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(posEle.Offset))
gl.VertexAttribDivisor(6, 1)
scaleEle := layout[6]
gl.EnableVertexAttribArray(7)
gl.VertexAttribPointer(7, scaleEle.ElementType.CompCount(), scaleEle.ElementType.GLType(), false, instancedBuf.Stride, gl.PtrOffset(scaleEle.Offset))
gl.VertexAttribDivisor(7, 1)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
instancedBuf.UnBind()
return &GlyphRend{
Atlas: atlas,
AtlasTex: &atlasTex,
GlyphMesh: glyphMesh,
InstancedBuf: instancedBuf,
GlyphMat: glyphMat,
GlyphCount: 0,
GlyphVBO: make([]float32, 0),
}, nil
}