[Из песочницы] freetype 2 и opengl пишем текст

Настало время как я разобрался с freetype2. Теперь я хочу сделать так, чтобы мой код стал доступен нуждающимся. Потому как обдумывать как работать с библиотекой не всегда есть время. Я хочу показать код работы именно с freetype и немного с opengl. Немного о коде. Я не могу создавать сложный код. У меня все получается как-то по простому. Я видел несколько исходников с freetype2 и ничего не мог понять. Как там все работает. Уж очень сложный код создавали авторы. Я надеюсь что мой простой код вам понравиться. После прочтения этой статьи можно будет создать многостроковый текст и отобразить одной текстурой на экран.
Итак, начнем.

Первое что я хотел бы здесь написать, так это шейдер, который я выписал из книги. Он накладывает двухмерную текстуру на несколько треугольников.

За создание шейдеров у меня отвечает отдельный класс. Я пишу ему какой шейдер скомпилировать и он мне возвращает программу. Также он добавляет в std: map контейнер программу по названию. Чтобы я мог в другом участке кода получить программу именно этого шейдера.

GLuint ShaderManager::createProgram ( const char *param )
{
        if ( !strncmp ( param, "sprite\0",7 ) ) {
                const char *vshader = 
                        "#version 300 es\n"
                        "layout(location = 0) in vec2 position;\n"
                        "layout(location = 1) in vec2 texCoord;\n"
                        "uniform mat4 transform;\n"
                        "out vec2 v_texCoord;\n"
                        "void main ( )\n"
                        "{\n"
                        " gl_Position = transform * vec4 ( position, 0.0, 1.0 );\n"
                        " v_texCoord = texCoord;\n"
                        "}";

                const char *fshader =
                        "#version 300 es\n"
                        "precision mediump float;\n"
                        "in vec2 v_texCoord;\n"
                        "layout(location = 0) out vec4 outColor;\n"
                        "uniform sampler2D s_texture;\n"
                        "void main ( )\n"
                        "{\n"
                        " outColor = texture ( s_texture, v_texCoord );\n"
                        "}";

                /* создать программу */
                GLuint program = loadProgram ( vshader, fshader );
                /* добавить программу в контейнер */
                global.programs["sprite"] = program;
                return program;


Далее я создал класс шрифта. Объект этого класса будет инициализировать текст, указывать позицию на экране и рисовать текстуру.

#ifndef H_FONT_H
#define H_FONT_H
#include 
#include 
#include 
#include 
#include 
#include 
#include "gl_mat.hpp"
#include "global.hpp"
#include 
#include FT_FREETYPE_H
#include FT_GLYPH_H

class Font {
        public:
                Font ( ) { }
                /* инициализировать библиотеку freetype и загрузить ttf файл. */
                Font ( const char *ttf_file );
                /* задать позицию на экране */
                void setPos ( int x, int y );
                /* здесь происходит создание текстуры. Вот параметры
                 *\1 сам текст в широких символах.
                 *\2 размер шрифта.
                 *\3 расстояние между шрифтами по горизонтали в пикселях.
                 *\4 расстояние между шрифтами по вертикали в пикселях.
                 *\5 размер пробела в пикселях.
                 *\6 компонент цвет красный.
                 *\7 компонент цвет зеленый.
                 *\8 компонент цвет синий.
                 * ну это значит что можно задать любой цвет тексту */
                void init ( wchar_t *text, int fontSize, int align, int valign, int space, uint8_t r, uint8_t g, uint8_t b );
                /* задать размер текстуры */
                void setSize ( int w, int h );
                /* рисовать текстуру */
                void draw ( );
        private:
                FT_Face face = 0;
                /* здесь текстурные координаты */
                float *texture;
                /* здесь координаты вершин */
                float *vertices;
                /* это размер текстуры : ширина */
                int width;
                /* это размер текстуры : высота */
                int height;
                /* это для шейдера надо */
                int sampler;
                /* id текстуры */
                GLuint textureid;
                /* координата x */
                int x;
                /* координата y */
                int y;
                /* это замена функции glOrtho */
                float ortho[4][4];
                /* это для перемещения на экране */
                float translate[4][4];
                /* здесь результат матрицы */
                float result[4][4];
                /* шейдерная программа */
                unsigned int program;
                FT_Library ft_library;
                FT_Face ttf;
};
#endif


Ну вот, класс готов. Теперь приступим к реализации. Я делаю игру на android с sdl2 и тестирую на пк. Поэтому я знаю один единственный способ как отобразить данные на экран используя gles2 и opengl.

Код я прокомментирую. Вроде там нет ошибок. И мне он так нравиться что вообще супер. Итак начнем.

#include "font.hpp"

Font::Font ( const char *ttf_file )
{
        /* Начнем с создании объекта и подготовки данных
         * так как я делаю игру для android, я не использую cpp библиотеку glm.
         * Я создал альтернативу, сишную библиотеку и пока там только три или четыре функции 
         * ну здесь идет очистка массивов в ноль */
        glm::clearMatrix4x4 ( &ortho[0] );
        glm::clearMatrix4x4 ( &translate[0] );
        glm::clearMatrix4x4 ( &result[0] );

        /* получаю из глобальной структуры шейдерную программу */
        program = global.programs["sprite"];
        /* также в глобальной структуре хранятся размеры экрана, их я тоже использую */
        int width = global.width;
        int height = global.height;
        /* вот и пригодились размеры экрана, здесь я заполняю матрицу правильными значениями
         * для 2d рисунков */
        glm::ortho ( &ortho[0], 0.0f, width, 0.0f, height, 0.0f, 1.0f );
        /* устанавливаю позицию в ноль */
        setPos ( 0, 0 );

        /* инициализация библиотеки freetype2. */
        FT_Init_FreeType( &ft_library );

        /* здесь загружается файл шрифта */
#ifdef __ANDROID__
        FT_NewFace ( ft_library, ttf_file, 0, &face );
#else
        char *path = (char *) new char[255];
        sprintf ( path, "assets/%s", ttf_file );
        FT_New_Face ( ft_library, path, 0, &face );
        free ( path );
#endif
}

/* а вот здесь самое интересное */
void Font::init ( wchar_t *es, int fontSize, int align, int vert, int space, uint8_t r, uint8_t g, uint8_t b )
{
        /* задать размер пикселя в высоту */
        FT_Set_Pixel_Sizes ( face, 0, fontSize );

        FT_Glyph glyph;

        int w = 0;
        unsigned int h = 0;
        unsigned int maxh = 0;
        unsigned int toprow = 0;
        /* эта функция возвращает сколько символов в широкой строке, если например в строке
         * будут три буквы iаф, то функция вернет три символа. */
        int len = wcslen ( es );

        /* первое что я придумал это посчитать какую текстуру вообще надо создать, но для этого
         * мне пришлось создать каждый символ и узнать его ширину. Так я вижу полную картину. Знаю
         * какой массив создать */
        for ( int i = 0; i < len; i++ ) {
               /* итак получаем символ */
                wchar_t charcode = es[i];
                /* далее идут стандартные операции для создания bitmap символа */
                FT_Load_Char ( face, charcode, FT_LOAD_RENDER );

                FT_UInt glyph_index = FT_Get_Char_Index ( face, charcode )
                FT_Load_Glyph ( face, glyph_index, FT_LOAD_DEFAULT );
                FT_Render_Glyph ( face->glyph, FT_RENDER_MODE_NORMAL );
                FT_Get_Glyph ( face->glyph, &glyph );

                FT_Glyph_To_Bitmap ( &glyph, FT_RENDER_MODE_NORMAL, 0, 1 );
                FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) glyph;
                FT_Bitmap bitmap = bitmap_glyph->bitmap;
                /* теперь надо узнать ширину символа */
                w += bitmap.width;

                /* если высота меньше высоты символа */
                if ( h < bitmap.rows ) {
                        /* узнать разницу высоты шрифта и отступа от верха. */
                        int resize = bitmap.rows - bitmap_glyph->top;
                        /* теперь высота значиться как высота символа плюс отступ */
                        h = bitmap.rows + resize;
                        /* здесь надо знать самую большую высоту символа */
                        if ( toprow < bitmap.rows ) toprow = bitmap.rows;
                }
                /* здесь устанавливается максимальная высота вместе с отступом */
                if ( maxh < bitmap.rows + bitmap_glyph->top ) maxh = bitmap.rows + bitmap_glyph->top;

                /* если символ равен пробелу, то увеличить w на столько пикселей, сколько задали при 
                 * инициализации */
                if ( charcode == ' ' ) w += space;
                /* если встретился символ 'новая строка'
                 * то увеличить высоту включив туда вертикальный отступ и максимальную высоту */
                if ( charcode == '\n' ) { 
                        h += vert + maxh;
                        FT_Done_Glyph ( glyph );
                        continue;
                }
                /* это расстояние между шрифтом, если align равен одному пикселю, то увеличиться на один */
                w += align;

                FT_Done_Glyph ( glyph );
        }

        /* теперь можно создать подготовительный двумерный массив,
         * он включает размер всего текста в пикселях */
        uint8_t im[h][w];
        /* заполню нулями массив */
        memset ( &im[0][0], 0, w * h * sizeof ( uint8_t ) );

        int ih = 0;
        int iw = 0;
        int posy = 0;
        int topy = 0;
        int maxwidth = 0;
        for ( int i = 0; i < len; i++ ) {
                wchar_t charcode = es[i];
                FT_Load_Char ( face, charcode, FT_LOAD_RENDER );
                FT_UInt glyph_index = FT_Get_Char_Index ( face, charcode );

                FT_Load_Glyph ( face, glyph_index, FT_LOAD_DEFAULT );
                FT_Render_Glyph ( face->glyph, FT_RENDER_MODE_NORMAL );
                FT_Get_Glyph ( face->glyph, &glyph );

                FT_Glyph_To_Bitmap ( &glyph, FT_RENDER_MODE_NORMAL, 0, 1 );
                FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) glyph;
                FT_Bitmap bitmap = bitmap_glyph->bitmap;

                /* получить отступ символа от верха */
                posy = bitmap_glyph->top;
                /* это математика наверное, немогу объяснить как я тут высчитал */
                posy = bitmap.rows - posy; 
                topy = toprow - bitmap.rows;

                /* если новая строка, то ih - это высота от верха, то есть сверху это ноль,
                 * ниже увеличивается */
                if ( charcode == '\n' ) {
                        ih += maxh;
                        iw = 0;
                        FT_Done_Glyph ( glyph );
                        continue;
                }
                for ( unsigned int y = 0, i = 0; y < bitmap.rows; y++ ) {
                        for ( unsigned int x = 0; x < bitmap.width; x++, i++ ) {
                                /* здесь заполняется в нужное место один компонент цвета
                                 * пока массив из одного компонента gray, потом его перенесем в альфа канал */
                                im [ ih + posy + y + topy ] [ iw + x ] = bitmap.buffer[i];
                        }
                }
                /* увеличиваем ширину */
                iw += bitmap.width;
                /* увеличиваем расстояние между символами */
                iw += align;
                if ( maxwidth < iw ) maxwidth = iw;

                if ( charcode == ' ' ) {
                        iw += space;
                }

                FT_Done_Glyph ( glyph );

        }

        iw = maxwidth;
        width = iw;
        height = h;

        unsigned int size = width * height;
        /* а вот это уже будущая текстура */
        uint8_t *image_data = new uint8_t [ size * 4 ];
        /* заполняет белым цветом всю текстуру */
        memset ( image_data, 255, size * 4 * sizeof ( uint8_t ) );

        for ( unsigned int i = 0, y = 0; i < size; y++ ) {
                for ( int x = 0; x < width; x++, i++ ) {
                        /* сюда помещаем из нашего массива значение в альфа канал */
                        image_data[ 4 * i + 3] = im [ y ][ x ];
                        /* сюда цвет текста */
                        image_data[ 4 * i + 0] = r;
                        image_data[ 4 * i + 1] = g;
                        image_data[ 4 * i + 2] = b;
                }
        }

        /* стандартные действия для заполнения текстуры */
        glGenTextures ( 1, &textureid );
        glBindTexture ( GL_TEXTURE_2D, textureid );
        glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data );

        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);
        
        /* теперь нужно задать размер текстуры */
        setSize ( width, height );
        /* и удалить текстуру, она уже загружена в буфер и image_data больше не требуется. */
        delete[] image_data;


}
void Font::setSize ( int w, int h )
{
    /* это я высчитал, где должны быть размеры ширины и высоты, чтобы отобразить треугольники правильно */
    vertices = new float [ 12 ];
    vertices[0] = 0;
    vertices[1] = 0;
    vertices[2] = 0;
    vertices[3] = h;
    vertices[4] = w; 
    vertices[5] = 0;

    vertices[6] = w; 
    vertices[7] = 0;
    vertices[8] = w;
    vertices[9] = h;
    vertices[10] = 0;
    vertices[11] = h;

    /* для текстуры надо задавать полный размер в единицу, так она будет полностью наложена на
     * треугольники */
    texture = new float [ 12 ];
    texture[0] = 0;
    texture[1] = 1;
    texture[2] = 0;
    texture[3] = 0;
    texture[4] = 1;
    texture[5] = 1;

    texture[6] = 1;
    texture[7] = 1;
    texture[8] = 1;
    texture[9] = 0;
    texture[10] = 0;
    texture[11] = 0;
}

void Font::setPos ( int x, int y )
{
        /* ну здесь задается позиция, где отобразить текст */
        this->x = x;
        this->y = y;
        glm::translate ( &translate[0], x, y, 0 );
        glm::sumMatrix ( &result[0], &translate[0], &ortho[0] );
}

void Font::draw ( )
{
       /* стандартные действия для использования шейдера */
        glUseProgram ( program );

        sampler = glGetUniformLocation ( program, "s_texture" );

        glActiveTexture ( GL_TEXTURE0 );
        glBindTexture ( GL_TEXTURE_2D, textureid );
        glUniform1i ( sampler, 0 );

        GLint projection_location = glGetUniformLocation ( program, "transform" );
        glUniformMatrix4fv ( projection_location, 1, GL_FALSE, &result[0][0] );

        glEnableVertexAttribArray ( 0 );
        glEnableVertexAttribArray ( 1 );

        /* сюда заноситься координаты вершин */
        glVertexAttribPointer ( 0, 2, GL_FLOAT, GL_FALSE, 0, vertices );
        /* сюда заноситься координаты текстуры */
        glVertexAttribPointer ( 1, 2, GL_FLOAT, GL_FALSE, 0, texture );

        /* и рисуем текстуру */
        glDrawArrays ( GL_TRIANGLES, 0, 12 );

        glDisableVertexAttribArray ( 0 );
        glDisableVertexAttribArray ( 1 );
}


Из кода можно вызвать эту функцию вот так.

 Font *font = new Font ("anonymous.ttf");
        wchar_t * text = L"привет habr. Я тут статью написал. Она о freetype и opengl.\n"
                                    "С помощью freetype можно выводить текст.\n"
                                    "А с помощью моего кода, можно вывести несколько строк в одной текстуре";
        font->init ( text, 21, 1, 4, 4, 0, 0, 0 );
        font->setPos ( 100, 100 );


image

© Habrahabr.ru