Search code examples
pythonopenglpyopenglfreetypetext-rendering

How to render text with PyOpenGL?


I'm learning modern openGL, and at this moment I'm facing trouble with rendering text. I'm following this tutorial which is in C++, but I'm trying to implement in python.

Here is my code:

from OpenGL.GL import *
from OpenGL.GLU import *

from OpenGL.GL import shaders

import glfw
import freetype
import glm

import numpy as np
from PIL import Image
import math
import time


class CharacterSlot:
    def __init__(self, texture, glyph):
        self.texture = texture
        self.textureSize = (glyph.bitmap.width, glyph.bitmap.rows)

        if isinstance(glyph, freetype.GlyphSlot):
            self.bearing = (glyph.bitmap_left, glyph.bitmap_top)
            self.advance = glyph.advance.x
        elif isinstance(glyph, freetype.BitmapGlyph):
            self.bearing = (glyph.left, glyph.top)
            self.advance = None
        else:
            raise RuntimeError('unknown glyph type')

def _get_rendering_buffer(xpos, ypos, w, h, zfix=0.0):
    return np.asarray([
        xpos, ypos - h, zfix, 0.0, 1.0,
        xpos, ypos, zfix, 0.0, 0.0,
        xpos + w, ypos, zfix, 1.0, 0.0,
        xpos, ypos - h, zfix, 0.0, 1.0,
        xpos + w, ypos, zfix, 1.0, 0.0,
        xpos + w, ypos - h, zfix, 1.0, 1.0
    ], np.float32)


VERTEX_SHADER = """
        #version 330 core
        layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
        out vec2 TexCoords;

        uniform mat4 projection;

        void main()
        {
            gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
            TexCoords = vertex.zw;
        }


       """

FRAGMENT_SHADER = """
        #version 330 core
        in vec2 TexCoords;
        out vec4 color;

        uniform sampler2D text;
        uniform vec3 textColor;

        void main()
        {    
            vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
            color = vec4(textColor, 1.0) * sampled;
        }

        """

shaderProgram = None
Characters = dict()
VBO = None
VAO = None

def initliaze():
    global VERTEXT_SHADER
    global FRAGMENT_SHADER
    global shaderProgram
    global Characters
    global VBO
    global VAO

    #compiling shaders
    vertexshader = shaders.compileShader(VERTEX_SHADER, GL_VERTEX_SHADER)
    fragmentshader = shaders.compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER)

    #creating shaderProgram
    shaderProgram = shaders.compileProgram(vertexshader, fragmentshader)

    #get projection
    #problem
    
    shader_projection = glGetUniformLocation(shaderProgram, "projection")
    projection = glm.ortho(0.0,640,0.0,640)
    glUniformMatrix4fv(shader_projection, 1, GL_FALSE, glm.value_ptr(projection));
    
    #disable byte-alignment restriction
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    face = freetype.Face("Vera.ttf")
    face.set_char_size( 48*64 )

    #load first 128 characters of ASCII set
    for i in range(0,128):
        face.load_char(chr(i))
        glyph = face.glyph
        
        #generate texture
        texture = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture)
        glTexImage2D(GL_TEXTURE_2D,
                     0,
                     GL_RGB,
                     glyph.bitmap.width, glyph.bitmap.rows,
                     0,
                     GL_RGB,
                     GL_UNSIGNED_BYTE,
                     glyph.bitmap.buffer)

        #texture options
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        #now store character for later use
        Characters[chr(i)] = CharacterSlot(texture,glyph)
        
    glBindTexture(GL_TEXTURE_2D, 0);

    #configure VAO/VBO for texture quads
    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)
    
    VBO = glGenBuffers(1);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, 6 * 4, None, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    
def render_text(window,text,x,y,scale,color):
    global shaderProgram
    global Characters
    global VBO
    global VAO
    
    face = freetype.Face("Vera.ttf")
    face.set_char_size(48*64)
    glUniform3f(glGetUniformLocation(shaderProgram, "textColor"),
                color[0]/255,color[1]/255,color[2]/255)
               
    glActiveTexture(GL_TEXTURE0);
    

    for c in text:
        ch = Characters[c]
        w,h = ch.textureSize
        w = w*scale
        h = w*scale
        vertices = _get_rendering_buffer(x,y,w,h)

        #render glyph texture over quad
        glBindTexture(GL_TEXTURE_2D, ch.texture);
        #update content of VBO memory
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, len(vertices), vertices)

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        #render quad
        glDrawArrays(GL_TRIANGLES, 0, 6);
        #now advance cursors for next glyph (note that advance is number of 1/64 pixels)
        x += (ch.advance+6)*scale;

    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    glfwSwapBuffers(window);
    glfwPollEvents();
    
def main():
    glfw.init()
    window = glfw.create_window(640, 640,"EXAMPLE PROGRAM",None,None)
    
    glfw.make_context_current(window)

    
    initliaze()
    while not glfw.window_should_close(window):
        glfw.poll_events()
        glClearColor(0,0,0,1);
        glClear(GL_COLOR_BUFFER_BIT);
        render_text(window,'hello',1,1,1,(100,100,100))

        
    glfw.terminate()


if __name__ == '__main__':
    main()

I'm facing trouble in two portion so far I can understand. The first problem in initliaze(), error raised for the following portion.

    shader_projection = glGetUniformLocation(shaderProgram, "projection")
    projection = glm.ortho(0.0,640,0.0,640)
    glUniformMatrix4fv(shader_projection, 1, GL_FALSE, glm.value_ptr(projection));

I've commented out the above portion to ignore. The second problem is in render_text() function, error raised for the following portion.

glUniform3f(glGetUniformLocation(shaderProgram, "textColor"),
                color[0]/255,color[1]/255,color[2]/255)

There could be problems in many more places. I don't understand that why text rendering will be so difficult. What am I missing here?


Solution

  • You missed to install the shader program by glUseProgram:

    shaderProgram = shaders.compileProgram(vertexshader, fragmentshader)
    glUseProgram(shaderProgram) # <---
    

    The 2nd argument to glBufferData and the 3rd argument of glBufferSubData is the size in bytes:

    glBufferData(GL_ARRAY_BUFFER, 6 * 4, None, GL_DYNAMIC_DRAW)

    glBufferData(GL_ARRAY_BUFFER, 6 * 4 * 4, None, GL_DYNAMIC_DRAW)
    

    glBufferSubData(GL_ARRAY_BUFFER, 0, len(vertices), vertices)

    glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
    

    The vertex attribute consist of a 2 dimension vertex coordinate (x, y) and a 2 dimensional texture coordinate. Remove the wird zfix from the array of vertex attribute data. Furthermore you have to flip the 2nd component of the texture coordinates (otherwise the text is upside down)

    def _get_rendering_buffer(xpos, ypos, w, h, zfix=0.0):
        return np.asarray([
            xpos,     ypos - h, 0, 0,
            xpos,     ypos,     0, 1,
            xpos + w, ypos,     1, 1,
            xpos,     ypos - h, 0, 0,
            xpos + w, ypos,     1, 1,
            xpos + w, ypos - h, 1, 0
        ], np.float32)
    

    The stride argument of glVertexAttribIPointer has to be specified in bytes. If stride is 0, the generic vertex attributes are understood to be tightly packed in the array. Hence in your case stride has to be 16 or 0:

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4, 0)

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)
    

    face.load_char(chr(i)) generates a image with on color channel (1 byte per pixel). Use the internal format and format GL_RED rather than GL_RGB for generating the 2 dimensional texture image:

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, glyph.bitmap.width, glyph.bitmap.rows, 0,
                 GL_RED, GL_UNSIGNED_BYTE, glyph.bitmap.buffer) 
    

    You have to bind the vertex array, before drawing the text:

    glBindVertexArray(VAO)
    for c in text:
        # [...]
    
        glDrawArrays(GL_TRIANGLES, 0, 6)
    

    There is typo when you increment x, you have to use the >>-operator rather than the +-operator:

    x += (ch.advance+6)*scale

    x += (ch.advance>>6)*scale
    

    and another typo when you compute h:

    h = w*scale

    h = h*scale
    

    You have to enable alpha blending:

    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    

    In NDC (normalized device coordinates) the left bottom is (-1, -1) and the right top is (1, 1). Set the orthographic projection in that way, that the top left of the window is at (0, 0):

    projection = glm.ortho(0.0,640,0.0,640)

    projection = glm.ortho(0, 640, 640, 0)
    

    The reference point of the text is at the bottom. Hence you have to set a x coordinate greater than the text height:

    render_text(window,'hello',1,1,1,(100,100,100))

    render_text(window,'hello', 20, 50, 1, (255, 100, 100))
    

    See the complete example (I've used a different font):

    from OpenGL.GL import *
    from OpenGL.GLU import *
    
    from OpenGL.GL import shaders
    
    import glfw
    import freetype
    import glm
    
    import numpy as np
    from PIL import Image
    import math
    import time
    
    
    fontfile = "Vera.ttf"
    #fontfile = r'C:\source\resource\fonts\gnu-freefont_freesans\freesans.ttf'
    
    class CharacterSlot:
        def __init__(self, texture, glyph):
            self.texture = texture
            self.textureSize = (glyph.bitmap.width, glyph.bitmap.rows)
    
            if isinstance(glyph, freetype.GlyphSlot):
                self.bearing = (glyph.bitmap_left, glyph.bitmap_top)
                self.advance = glyph.advance.x
            elif isinstance(glyph, freetype.BitmapGlyph):
                self.bearing = (glyph.left, glyph.top)
                self.advance = None
            else:
                raise RuntimeError('unknown glyph type')
    
    def _get_rendering_buffer(xpos, ypos, w, h, zfix=0.0):
        return np.asarray([
            xpos,     ypos - h, 0, 0,
            xpos,     ypos,     0, 1,
            xpos + w, ypos,     1, 1,
            xpos,     ypos - h, 0, 0,
            xpos + w, ypos,     1, 1,
            xpos + w, ypos - h, 1, 0
        ], np.float32)
    
    
    VERTEX_SHADER = """
            #version 330 core
            layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
            out vec2 TexCoords;
    
            uniform mat4 projection;
    
            void main()
            {
                gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
                TexCoords = vertex.zw;
            }
           """
    
    FRAGMENT_SHADER = """
            #version 330 core
            in vec2 TexCoords;
            out vec4 color;
    
            uniform sampler2D text;
            uniform vec3 textColor;
    
            void main()
            {    
                vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
                color = vec4(textColor, 1.0) * sampled;
            }
            """
    
    shaderProgram = None
    Characters = dict()
    VBO = None
    VAO = None
    
    def initliaze():
        global VERTEXT_SHADER
        global FRAGMENT_SHADER
        global shaderProgram
        global Characters
        global VBO
        global VAO
    
        #compiling shaders
        vertexshader = shaders.compileShader(VERTEX_SHADER, GL_VERTEX_SHADER)
        fragmentshader = shaders.compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER)
    
        #creating shaderProgram
        shaderProgram = shaders.compileProgram(vertexshader, fragmentshader)
        glUseProgram(shaderProgram)
    
        #get projection
        #problem
        
        shader_projection = glGetUniformLocation(shaderProgram, "projection")
        projection = glm.ortho(0, 640, 640, 0)
        glUniformMatrix4fv(shader_projection, 1, GL_FALSE, glm.value_ptr(projection))
        
        #disable byte-alignment restriction
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    
        face = freetype.Face(fontfile)
        face.set_char_size( 48*64 )
    
        #load first 128 characters of ASCII set
        for i in range(0,128):
            face.load_char(chr(i))
            glyph = face.glyph
            
            #generate texture
            texture = glGenTextures(1)
            glBindTexture(GL_TEXTURE_2D, texture)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, glyph.bitmap.width, glyph.bitmap.rows, 0,
                         GL_RED, GL_UNSIGNED_BYTE, glyph.bitmap.buffer)
    
            #texture options
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    
            #now store character for later use
            Characters[chr(i)] = CharacterSlot(texture,glyph)
            
        glBindTexture(GL_TEXTURE_2D, 0)
    
        #configure VAO/VBO for texture quads
        VAO = glGenVertexArrays(1)
        glBindVertexArray(VAO)
        
        VBO = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, VBO)
        glBufferData(GL_ARRAY_BUFFER, 6 * 4 * 4, None, GL_DYNAMIC_DRAW)
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
        glBindVertexArray(0)
        
    def render_text(window,text,x,y,scale,color):
        global shaderProgram
        global Characters
        global VBO
        global VAO
        
        face = freetype.Face(fontfile)
        face.set_char_size(48*64)
        glUniform3f(glGetUniformLocation(shaderProgram, "textColor"),
                    color[0]/255,color[1]/255,color[2]/255)
                   
        glActiveTexture(GL_TEXTURE0)
        
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    
        glBindVertexArray(VAO)
        for c in text:
            ch = Characters[c]
            w, h = ch.textureSize
            w = w*scale
            h = h*scale
            vertices = _get_rendering_buffer(x,y,w,h)
    
            #render glyph texture over quad
            glBindTexture(GL_TEXTURE_2D, ch.texture)
            #update content of VBO memory
            glBindBuffer(GL_ARRAY_BUFFER, VBO)
            glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
    
            glBindBuffer(GL_ARRAY_BUFFER, 0)
            #render quad
            glDrawArrays(GL_TRIANGLES, 0, 6)
            #now advance cursors for next glyph (note that advance is number of 1/64 pixels)
            x += (ch.advance>>6)*scale
    
        glBindVertexArray(0)
        glBindTexture(GL_TEXTURE_2D, 0)
    
        glfw.swap_buffers(window)
        glfw.poll_events()
        
    def main():
        glfw.init()
        window = glfw.create_window(640, 640,"EXAMPLE PROGRAM",None,None)    
        glfw.make_context_current(window)
        
        initliaze()
        while not glfw.window_should_close(window):
            glfw.poll_events()
            glClearColor(0,0,0,1)
            glClear(GL_COLOR_BUFFER_BIT)
            render_text(window,'hello', 20, 50, 1, (255, 100, 100))
    
        glfw.terminate()
    
    if __name__ == '__main__':
        main()
    

    See also FreeType / OpenGL text rendering