FreeType - Texture Atlas - Why is my text rendering as quad?

2.3k Views Asked by At

I'm currently trying to improve my text rendering. The basic way where you render every character separate works, but now I want to do everything in one draw call by rendering a texture atlas. The texture atlas is almost done. It does render the text as quad, but I cannot figure out how to solve the alpha problem. The values R, G and B are all zero.

I have also tried GL_ALPHA, GL_RGBA and GL_LUMINANCE.

Note: I'm working on a Raspberry Pi and using OpenGL ES 2.0.

Image of all font characters: An image of the text

Shaders:

precision mediump float;

attribute vec4 vertex;

varying vec2 textCoord;

void main()
{
   gl_Position = vec4(vertex.xy, 0.0, 1.0);
   textCoord = vertex.zw;
}
precision lowp float; 

varying vec2 textCoord;

uniform sampler2D text;
uniform vec3 textColor;

void main()
{
    lowp vec4 sampled = vec4(1.0, 1.0, texture2D(text, textCoord).ba);
    gl_FragColor = vec4(textColor, 1.0) * sampled;
}

Setup Texture Atlas:

glGenBuffers(1, &vbo);

if (FT_Init_FreeType(&m_FT)) {
    std::cout << "ERROR: Could not init the FreeType Library" << std::endl;
}

if (FT_New_Face(m_FT, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 0, &m_Face)) {
    std::cout << "ERROR: This font failed to load." << std::endl;
}

FT_Set_Pixel_Sizes(m_Face, 0, height);

unsigned int roww = 0;
unsigned int rowh = 0;

memset(c, 0, sizeof c); // Set all values to 0

// Find minimum size for a texture holding all visible ASCII characters 
for (int i = 0; i < 128; i++) {
    if (FT_Load_Char(m_Face, i, FT_LOAD_RENDER)) {
        fprintf(stderr, "Loading character %c failed!\n", i);
        continue;
    }
    if (roww + m_Face->glyph->bitmap.width + 1 >= SCREEN_WIDTH) {
        w = std::max(w, roww);
        h += rowh;
        roww = 0;
        rowh = 0;
    }
    roww += m_Face->glyph->bitmap.width + 1;
    rowh = std::max(rowh, m_Face->glyph->bitmap.rows);
}

w = std::max(w, roww);
h += rowh;

// Create a texture that will be used to hold all ASCII glyphs 
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);

GetShader()->Use();
GetShader()->SetInt("text", tex);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);

// We require 1 byte alignment when uploading texture data 
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

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);

// Paste all glyph bitmaps into the texture, remembering the offset 
int ox = 0;
int oy = 0;

rowh = 0;

for (int i = 0; i < 128; i++) {
    if (FT_Load_Char(m_Face, i, FT_LOAD_RENDER)) {
        fprintf(stderr, "Loading character %c failed!\n", i);
        continue;
    }

    if (ox + m_Face->glyph->bitmap.width + 1 >= SCREEN_WIDTH) {
        oy += rowh;
        rowh = 0;
        ox = 0;
    }


    glTexSubImage2D(GL_TEXTURE_2D, 0, ox, oy, m_Face->glyph->bitmap.width, m_Face->glyph->bitmap.rows, GL_RGBA, GL_UNSIGNED_BYTE, m_Face->glyph->bitmap.buffer);
    c[i].ax = m_Face->glyph->advance.x >> 6;
    c[i].ay = m_Face->glyph->advance.y >> 6;

    c[i].bw = m_Face->glyph->bitmap.width;
    c[i].bh = m_Face->glyph->bitmap.rows;

    c[i].bl = m_Face->glyph->bitmap_left;
    c[i].bt = m_Face->glyph->bitmap_top;

    c[i].tx = ox / (float)w;
    c[i].ty = oy / (float)h;

    rowh = std::max(rowh, m_Face->glyph->bitmap.rows);
    ox += m_Face->glyph->bitmap.width + 1;
}

fprintf(stderr, "Generated a %d x %d (%d kb) texture atlas\n", w, h, w * h / 1024);

GetShader()->Stop();

glBindTexture(GL_TEXTURE_2D, 0);

Draw function:

// Set uniforms
    a_Shader->SetVec3("textColor", m_v3Color);

    glActiveTexture(GL_TEXTURE0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

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

    point coords[6 * 128];
    int dc = 0;

    const uint8_t *p;

    float sx = 2.0 / SCREEN_WIDTH;
    float sy = 2.0 / SCREEN_HEIGHT;

//  float x = (m_v2Position.x - (SCREEN_WIDTH / 2)) * sx;
    //float y = (m_v2Position.y - (SCREEN_HEIGHT / 2)) * sy;

    float x = -1.f;
    float y = 0.f;

    // Loop through all characters 
    for (int p = 0; p < 128; p++) {
        // Calculate the vertex and texture coordinates 
        float x2 = x + c[p].bl * sx;
        float y2 = -y - c[p].bt * sy;
        float w = c[p].bw * sx;
        float h = c[p].bh * sy;

        // Advance the cursor to the start of the next character 
        x += c[p].ax * sx;
        y += c[p].ay * sy;

        // Skip glyphs that have no pixels 
        if (!w || !h)
            continue;

        coords[dc++] = (point) {
            x2, -y2, c[p].tx, c[p].ty
        };
        coords[dc++] = (point) {
            x2 + w, -y2, c[p].tx + c[p].bw / w, c[p].ty
        };
        coords[dc++] = (point) {
            x2, -y2 - h, c[p].tx, c[p].ty + c[p].bh / h
        };
        coords[dc++] = (point) {
            x2 + w, -y2, c[p].tx + c[p].bw / w, c[p].ty
        };
        coords[dc++] = (point) {
            x2, -y2 - h, c[p].tx, c[p].ty + c[p].bh / h
        };
        coords[dc++] = (point) {
            x2 + w, -y2 - h, c[p].tx + c[p].bw / w, c[p].ty + c[p].bh / h
        };
    }

    // Render glyph texture over quad
    glBindTexture(GL_TEXTURE_2D, tex);

    // Update content of VBO memory
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(coords), coords, GL_DYNAMIC_DRAW); 
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // Render quad
    glDrawArrays(GL_TRIANGLES, 0, dc);

    glDisableVertexAttribArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    glCheckError(); 
1

There are 1 best solutions below

6
Rabbid76 On

The buffer which is provided by m_Face->glyph->bitmap.buffer is a buffer with one single color channel. Since OpenGL ES is used, the source format of the texture has to be GL_LUMINANCE.

Specify a two-dimensional texture image with a single (red) color channel. The alignment for a row of the texture has to be set 1 (see glPixelStorei). Note default is 4, which doesn't match a tightly packed texture with a size of 1 byte per pixel:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w, h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);

Note, since the texture is non power of 2, mip mapmapping can't be used, so the texture minifying function has to be either GL_NEAREST or GL_LINEAR and the wrap mode has to be GL_CLAMP_TO_EDGE:

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);

Load the glyphs to the texture:

glTexSubImage2D(
    GL_TEXTURE_2D, 0, ox, oy, 
    m_Face->glyph->bitmap.width, m_Face->glyph->bitmap.rows,
    GL_LUMINANCE, GL_UNSIGNED_BYTE, m_Face->glyph->bitmap.buffer);

Since the internal texture format is GL_LUMINANCE, the samples have to be read form the red, green or blue color channel:

lowp float sampled = texture2D(text, textCoord).r;
gl_FragColor = vec4(textColor, 1.0) * sampled;