├── README.md ├── truetype.go ├── shader.go └── font.go /README.md: -------------------------------------------------------------------------------- 1 | glfont for gles2 2 | 3 | 4 | # Example: 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | // "fmt" 11 | "log" 12 | "runtime" 13 | "fmt" 14 | "bytes" 15 | "github.com/gobuffalo/packr" 16 | 17 | "github.com/go-gl/gl/v2.1/gl" 18 | "github.com/go-gl/glfw/v3.1/glfw" 19 | // "github.com/nullboundary/glfont" 20 | "github.com/liamg/aminal/glfont" 21 | 22 | ) 23 | 24 | const windowWidth = 500 25 | const windowHeight = 400 26 | 27 | func init() { 28 | runtime.LockOSThread() 29 | } 30 | 31 | func loadFont(name string,scale float32, windowWidth int, windowHeight int) (*glfont.Font, error) { 32 | box := packr.NewBox("/Users/evil/dev/go/aminal/gui/packed-fonts/") 33 | fontBytes, err := box.Find(name) 34 | if err != nil { 35 | return nil, fmt.Errorf("packaged font '%s' could not be read: %s", name, err) 36 | } 37 | 38 | font, err := glfont.LoadFont(bytes.NewReader(fontBytes), scale, windowWidth, windowHeight) 39 | if err != nil { 40 | return nil, fmt.Errorf("font '%s' failed to load: %v", name, err) 41 | } 42 | 43 | return font, nil 44 | } 45 | 46 | func main() { 47 | 48 | if err := glfw.Init(); err != nil { 49 | log.Fatalln("failed to initialize glfw:", err) 50 | } 51 | defer glfw.Terminate() 52 | window, _ := glfw.CreateWindow(int(windowWidth), int(windowHeight), "glfontExample", nil, nil) 53 | 54 | window.MakeContextCurrent() 55 | glfw.SwapInterval(1) 56 | 57 | if err := gl.Init(); err != nil { 58 | panic(err) 59 | } 60 | 61 | //load font (fontfile, font scale, window width, window height 62 | font, err := loadFont("Hack Regular Nerd Font Complete.ttf", 18, windowWidth, windowHeight) 63 | if err != nil { 64 | log.Panicf("LoadFont: %v", err) 65 | } 66 | 67 | gl.Enable(gl.DEPTH_TEST) 68 | gl.DepthFunc(gl.LESS) 69 | gl.ClearColor(0.0, 0.0, 0.0, 0.0) 70 | 71 | for !window.ShouldClose() { 72 | gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 73 | 74 | //set color and draw text 75 | font.SetColor(1.0, 1.0, 1.0, 1.0) //r,g,b,a font color 76 | font.Print(4, 100, "hello,world") //x,y,scale,string,printf args 77 | 78 | window.SwapBuffers() 79 | glfw.PollEvents() 80 | 81 | } 82 | } 83 | ``` 84 | 85 | #### Contributors 86 | * [evilbinary](https://github.com/evilbinary) 87 | * [kivutar](https://github.com/kivutar) 88 | -------------------------------------------------------------------------------- /truetype.go: -------------------------------------------------------------------------------- 1 | package glfont 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | // "github.com/go-gl/gl/all-core/gl" 8 | "github.com/go-gl/gl/v2.1/gl" 9 | "github.com/golang/freetype/truetype" 10 | "golang.org/x/image/font" 11 | ) 12 | 13 | type character struct { 14 | textureID uint32 // ID handle of the glyph texture 15 | width int //glyph width 16 | height int //glyph height 17 | advance int //glyph advance 18 | bearingH int //glyph bearing horizontal 19 | bearingV int //glyph bearing vertical 20 | } 21 | 22 | //LoadTrueTypeFont builds a set of textures based on a ttf files glyphs 23 | func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error) { 24 | data, err := ioutil.ReadAll(r) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | //make Font struct type 30 | f := new(Font) 31 | f.scale = scale 32 | f.characters = map[rune]*character{} 33 | f.program = program //set shader program 34 | // Read the truetype font. 35 | f.ttf, err = truetype.Parse(data) 36 | if err != nil { 37 | return nil, err 38 | } 39 | f.SetColor(1.0, 1.0, 1.0, 1.0) //set default white 40 | 41 | _, h := f.MaxSize() 42 | f.lineHeight = h 43 | 44 | gl.BindTexture(gl.TEXTURE_2D, 0) 45 | 46 | // Configure VAO/VBO for texture quads 47 | gl.GenVertexArrays(1, &f.vao) 48 | gl.GenBuffers(1, &f.vbo) 49 | gl.BindVertexArray(f.vao) 50 | gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) 51 | 52 | gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.STATIC_DRAW) 53 | 54 | vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00"))) 55 | gl.EnableVertexAttribArray(vertAttrib) 56 | gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0)) 57 | //defer gl.DisableVertexAttribArray(vertAttrib) 58 | 59 | texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00"))) 60 | gl.EnableVertexAttribArray(texCoordAttrib) 61 | gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4)) 62 | //defer gl.DisableVertexAttribArray(texCoordAttrib) 63 | 64 | gl.BindBuffer(gl.ARRAY_BUFFER, 0) 65 | gl.BindVertexArray(0) 66 | 67 | //create new face to measure glyph dimensions 68 | f.ttfFace = truetype.NewFace(f.ttf, &truetype.Options{ 69 | Size: float64(f.scale), 70 | DPI: DPI, 71 | Hinting: font.HintingFull, 72 | }) 73 | 74 | return f, nil 75 | } 76 | -------------------------------------------------------------------------------- /shader.go: -------------------------------------------------------------------------------- 1 | package glfont 2 | 3 | import ( 4 | // "github.com/go-gl/gl/all-core/gl" 5 | "github.com/go-gl/gl/v2.1/gl" 6 | 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | //newProgram links the frag and vertex shader programs 12 | func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) { 13 | vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER) 14 | if err != nil { 15 | return 0, err 16 | } 17 | 18 | fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER) 19 | if err != nil { 20 | return 0, err 21 | } 22 | 23 | program := gl.CreateProgram() 24 | 25 | gl.AttachShader(program, vertexShader) 26 | gl.AttachShader(program, fragmentShader) 27 | gl.LinkProgram(program) 28 | 29 | var status int32 30 | gl.GetProgramiv(program, gl.LINK_STATUS, &status) 31 | if status == gl.FALSE { 32 | var logLength int32 33 | gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength) 34 | 35 | log := strings.Repeat("\x00", int(logLength+1)) 36 | gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log)) 37 | 38 | return 0, fmt.Errorf("failed to link program: %v", log) 39 | } 40 | 41 | gl.DeleteShader(vertexShader) 42 | gl.DeleteShader(fragmentShader) 43 | 44 | return program, nil 45 | } 46 | 47 | //compileShader compiles the shader program 48 | func compileShader(source string, shaderType uint32) (uint32, error) { 49 | shader := gl.CreateShader(shaderType) 50 | 51 | csources, free := gl.Strs(source) 52 | gl.ShaderSource(shader, 1, csources, nil) 53 | free() 54 | gl.CompileShader(shader) 55 | 56 | var status int32 57 | gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) 58 | if status == gl.FALSE { 59 | var logLength int32 60 | gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength) 61 | 62 | log := strings.Repeat("\x00", int(logLength+1)) 63 | gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log)) 64 | 65 | return 0, fmt.Errorf("failed to compile %v: %v", source, log) 66 | } 67 | 68 | return shader, nil 69 | } 70 | 71 | var fragmentFontShader = ` 72 | varying vec2 fragTexCoord; 73 | uniform sampler2D tex; 74 | uniform vec4 textColor; 75 | void main() 76 | { 77 | vec4 sampled = vec4(1.0, 1.0, 1.0, texture2D(tex, fragTexCoord).r); 78 | gl_FragColor= textColor * sampled; 79 | // gl_FragColor = texture2D(tex, fragTexCoord); 80 | //gl_FragColor = vec4(1.0,0.0,0.0,1.0); 81 | }` + "\x00" 82 | 83 | var vertexFontShader = ` 84 | 85 | //vertex position 86 | attribute vec2 vert; 87 | 88 | //pass through to fragTexCoord 89 | attribute vec2 vertTexCoord; 90 | 91 | //window res 92 | uniform vec2 resolution; 93 | 94 | //pass to frag 95 | varying vec2 fragTexCoord; 96 | 97 | void main() { 98 | // convert the rectangle from pixels to 0.0 to 1.0 99 | vec2 zeroToOne = vert / resolution; 100 | 101 | // convert from 0->1 to 0->2 102 | vec2 zeroToTwo = zeroToOne * 2.0; 103 | 104 | // convert from 0->2 to -1->+1 (clipspace) 105 | vec2 clipSpace = zeroToTwo - 1.0; 106 | 107 | fragTexCoord = vertTexCoord; 108 | 109 | gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); 110 | //gl_Position = vec4(10.1, 10.1,0.1,1.0); 111 | }` + "\x00" 112 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | package glfont 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | "io" 8 | 9 | // "github.com/go-gl/gl/all-core/gl" 10 | "github.com/go-gl/gl/v2.1/gl" 11 | 12 | "github.com/golang/freetype" 13 | "github.com/golang/freetype/truetype" 14 | "golang.org/x/image/font" 15 | "golang.org/x/image/math/fixed" 16 | ) 17 | 18 | const DPI = 72 19 | 20 | // A Font allows rendering of text to an OpenGL context. 21 | type Font struct { 22 | characters map[rune]*character 23 | vao uint32 24 | vbo uint32 25 | program uint32 26 | texture uint32 // Holds the glyph texture id. 27 | color color 28 | ttf *truetype.Font 29 | ttfFace font.Face 30 | scale float32 31 | linePadding float32 32 | lineHeight float32 33 | } 34 | 35 | type color struct { 36 | r float32 37 | g float32 38 | b float32 39 | a float32 40 | } 41 | 42 | //LoadFont loads the specified font at the given scale. 43 | func LoadFont(reader io.Reader, scale float32, windowWidth int, windowHeight int) (*Font, error) { 44 | 45 | // Configure the default font vertex and fragment shaders 46 | program, err := newProgram(vertexFontShader, fragmentFontShader) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | // Activate corresponding render state 52 | gl.UseProgram(program) 53 | 54 | //set screen resolution 55 | resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00")) 56 | gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight)) 57 | gl.UseProgram(0) 58 | 59 | return LoadTrueTypeFont(program, reader, scale) 60 | } 61 | 62 | func (f *Font) Free() { 63 | for _, chr := range f.characters { 64 | gl.DeleteTextures(1, &chr.textureID) 65 | } 66 | 67 | gl.DeleteBuffers(1, &f.vbo) 68 | gl.DeleteVertexArrays(1, &f.vao) 69 | 70 | gl.DeleteProgram(f.program) 71 | 72 | f.vbo = 0 73 | f.vao = 0 74 | f.program = 0 75 | } 76 | 77 | //SetColor allows you to set the text color to be used when you draw the text 78 | func (f *Font) SetColor(red float32, green float32, blue float32, alpha float32) { 79 | f.color.r = red 80 | f.color.g = green 81 | f.color.b = blue 82 | f.color.a = alpha 83 | } 84 | 85 | func (f *Font) UpdateResolution(windowWidth int, windowHeight int) { 86 | gl.UseProgram(f.program) 87 | resUniform := gl.GetUniformLocation(f.program, gl.Str("resolution\x00")) 88 | gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight)) 89 | gl.UseProgram(0) 90 | //f.characters = map[rune]*character{} 91 | } 92 | 93 | func (f *Font) LineHeight() float32 { 94 | return f.lineHeight 95 | } 96 | 97 | func (f *Font) LinePadding() float32 { 98 | return f.linePadding 99 | } 100 | 101 | //Printf draws a string to the screen, takes a list of arguments like printf 102 | func (f *Font) Print(x, y float32, text string) error { 103 | 104 | indices := []rune(text) 105 | 106 | if len(indices) == 0 { 107 | return nil 108 | } 109 | 110 | //setup blending mode 111 | gl.Enable(gl.BLEND) 112 | gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 113 | // gl.Enable(gl.TEXTURE_2D); 114 | // Activate corresponding render state 115 | gl.UseProgram(f.program) 116 | //set text color 117 | gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("textColor\x00")), f.color.r, f.color.g, f.color.b, f.color.a) 118 | //set screen resolution 119 | //resUniform := gl.GetUniformLocation(f.program, gl.Str("resolution\x00")) 120 | //gl.Uniform2f(resUniform, float32(2560), float32(1440)) 121 | 122 | gl.ActiveTexture(gl.TEXTURE0) 123 | gl.BindVertexArray(f.vao) 124 | 125 | // Iterate through all characters in string 126 | for i := range indices { 127 | 128 | //get rune 129 | runeIndex := indices[i] 130 | 131 | //find rune in fontChar list 132 | ch, err := f.GetRune(runeIndex) 133 | if err != nil { 134 | return err // @todo ignore errors? 135 | } 136 | 137 | //calculate position and size for current rune 138 | xpos := x + float32(ch.bearingH) 139 | ypos := y - float32(+ch.height-ch.bearingV) 140 | w := float32(ch.width) 141 | h := float32(ch.height) 142 | 143 | //set quad positions 144 | var x1 = xpos 145 | var x2 = xpos + w 146 | var y1 = ypos 147 | var y2 = ypos + h 148 | 149 | //setup quad array 150 | var vertices = []float32{ 151 | // X, Y, Z, U, V 152 | // Front 153 | x1, y1, 0.0, 0.0, 154 | x2, y1, 1.0, 0.0, 155 | x1, y2, 0.0, 1.0, 156 | x1, y2, 0.0, 1.0, 157 | x2, y1, 1.0, 0.0, 158 | x2, y2, 1.0, 1.0} 159 | 160 | // Render glyph texture over quad 161 | gl.BindTexture(gl.TEXTURE_2D, ch.textureID) 162 | // Update content of VBO memory 163 | gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) 164 | 165 | //BufferSubData(target Enum, offset int, data []byte) 166 | gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(vertices)) // Be sure to use glBufferSubData and not glBufferData 167 | // Render quad 168 | gl.DrawArrays(gl.TRIANGLES, 0, 24) 169 | 170 | gl.BindBuffer(gl.ARRAY_BUFFER, 0) 171 | // Now advance cursors for next glyph (note that advance is number of 1/64 pixels) 172 | x += float32((ch.advance >> 6)) // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels)) 173 | 174 | } 175 | 176 | //clear opengl textures and programs 177 | gl.BindVertexArray(0) 178 | gl.BindTexture(gl.TEXTURE_2D, 0) 179 | gl.UseProgram(0) 180 | gl.Disable(gl.BLEND) 181 | 182 | return nil 183 | } 184 | 185 | //Width returns the width of a piece of text in pixels 186 | func (f *Font) Size(text string) (float32, float32) { 187 | 188 | var width float32 189 | var height float32 190 | 191 | indices := []rune(text) 192 | 193 | if len(indices) == 0 { 194 | return 0, 0 195 | } 196 | 197 | // Iterate through all characters in string 198 | for i := range indices { 199 | 200 | //get rune 201 | runeIndex := indices[i] 202 | 203 | //find rune in fontChar list 204 | ch, err := f.GetRune(runeIndex) 205 | if err != nil { 206 | continue 207 | } 208 | 209 | // Now advance cursors for next glyph (note that advance is number of 1/64 pixels) 210 | width += float32((ch.advance >> 6)) // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels)) 211 | 212 | // Now advance cursors for next glyph (note that advance is number of 1/64 pixels) 213 | if float32(ch.height)*f.scale > height { 214 | height = float32(ch.height) 215 | } 216 | } 217 | 218 | return width, height 219 | } 220 | 221 | func (f *Font) MaxSize() (float32, float32) { 222 | b := f.ttf.Bounds(fixed.Int26_6(f.scale)) 223 | return float32(b.Max.X - b.Min.X), float32(b.Max.Y - b.Min.Y) 224 | } 225 | 226 | func (f *Font) MinY() float32 { 227 | b := f.ttf.Bounds(fixed.Int26_6(f.scale)) 228 | return float32(b.Min.Y) 229 | } 230 | 231 | func (f *Font) MaxY() float32 { 232 | b := f.ttf.Bounds(fixed.Int26_6(f.scale)) 233 | return float32(b.Max.Y) 234 | } 235 | 236 | func (f *Font) GetRune(r rune) (*character, error) { 237 | 238 | cc, ok := f.characters[r] 239 | if ok { 240 | return cc, nil 241 | } 242 | 243 | char := new(character) 244 | 245 | gBnd, gAdv, ok := f.ttfFace.GlyphBounds(r) 246 | if ok != true { 247 | return nil, fmt.Errorf("ttf face glyphBounds error") 248 | } 249 | 250 | gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6) 251 | gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6) 252 | 253 | //if gylph has no diamensions set to a max value 254 | if gw == 0 || gh == 0 { 255 | gBnd = f.ttf.Bounds(fixed.Int26_6(f.scale)) 256 | gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6) 257 | gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6) 258 | 259 | //above can sometimes yield 0 for font smaller than 48pt, 1 is minimum 260 | if gw == 0 || gh == 0 { 261 | gw = 1 262 | gh = 1 263 | } 264 | } 265 | 266 | //The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y. 267 | gAscent := int(-gBnd.Min.Y) >> 6 268 | gdescent := int(gBnd.Max.Y) >> 6 269 | 270 | //set w,h and adv, bearing V and bearing H in char 271 | char.width = int(gw) 272 | char.height = int(gh) 273 | char.advance = int(gAdv) 274 | char.bearingV = gdescent 275 | char.bearingH = (int(gBnd.Min.X) >> 6) 276 | 277 | //create image to draw glyph 278 | fg, bg := image.White, image.Black 279 | rect := image.Rect(0, 0, int(gw), int(gh)) 280 | rgba := image.NewRGBA(rect) 281 | draw.Draw(rgba, rgba.Bounds(), bg, image.ZP, draw.Src) 282 | 283 | //create a freetype context for drawing 284 | c := freetype.NewContext() 285 | c.SetDPI(DPI) 286 | c.SetFont(f.ttf) 287 | c.SetFontSize(float64(f.scale)) 288 | c.SetClip(rgba.Bounds()) 289 | c.SetDst(rgba) 290 | c.SetSrc(fg) 291 | c.SetHinting(font.HintingFull) 292 | 293 | //set the glyph dot 294 | px := 0 - (int(gBnd.Min.X) >> 6) 295 | py := (gAscent) 296 | pt := freetype.Pt(px, py) 297 | 298 | // Draw the text from mask to image 299 | if _, err := c.DrawString(string(r), pt); err != nil { 300 | return nil, err 301 | } 302 | 303 | // Generate texture 304 | var texture uint32 305 | gl.GenTextures(1, &texture) 306 | gl.BindTexture(gl.TEXTURE_2D, texture) 307 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 308 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 309 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 310 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 311 | gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, 312 | gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) 313 | 314 | char.textureID = texture 315 | 316 | f.characters[r] = char 317 | 318 | return char, nil 319 | } 320 | --------------------------------------------------------------------------------