Настало время как я разобрался с 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 <stdint.h>
#include <ft2build.h>
#include <string>
#include <vector>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_opengles2.h>
#include "gl_mat.hpp"
#include "global.hpp"
#include <wchar.h>
#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 );
	delete[]  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;

                /* узнать разницу высоты шрифта и отступа от верха. */
		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

Комментарии (15)


  1. andreili
    28.02.2019 07:55

    Все просто, пока не возникает нужда во всяких там обводках и прочем ;)
    Вот мой пример реализации из игры: bitbucket.org/schooldaysteam/nice-boat/src/master/nice_boat/src/OpenGLFont.cpp


  1. Leopotam
    28.02.2019 09:41

    А раньше был запрещен репост с других ресурсов. Делаем из хабра копипасту, но зачем...


    1. xverizex Автор
      28.02.2019 10:47

      А это не репост. Это я на форуме отписался о проделанной работе. А здесь я статью написал.


      1. Leopotam
        28.02.2019 11:14

        Статью? Так это просто копия одного поста с конечным решением, на форуме и то больше информации и обсуждений.


        1. xverizex Автор
          28.02.2019 11:45

          Как это больше. Здесь даже код прокомментирован.


  1. al_sh
    28.02.2019 12:02
    +1

    т.е. статья о том, как поместить растр отданный фритэпом в опенгл текстуру? А зачем в RGB, почему не писать в альфу и не красить в шейдере? Почему не в атлас?


    1. xverizex Автор
      28.02.2019 12:21
      -1

      В rgba, чтобы цвет какой захочешь можно было установить. Там же параметры есть для rgb. Через шейдер статичный цвет. Как в атлас не пойму?


    1. Leopotam
      28.02.2019 12:28

      А зачем в RGB

      Потому что автор не в курсе про SDF и как это потом пригодится.
      и не красить в шейдере

      И про заливку цвета через вертекс-колор, наверное, тоже.


      1. xverizex Автор
        28.02.2019 12:56

        В android скорее всего это не прокатит. Я знаю про glVertexColorPointer и glVertexPointer и glTexCoordPointer. Но это без шейдеров.


        1. Leopotam
          28.02.2019 13:14

          Ну fixed pipeline давно эмулируется через встроенные шейдеры (а ниже gles2 сейчас живых девайсов вроде как нет), поэтому использовать его смысла нет. Льем цвета в вертексы и дальше пробрасываем из вертексного в фрагментный через интерполятор. Так делается везде, где требуется менять цвет глифов + всякие эффекты типа градиента.


      1. xverizex Автор
        28.02.2019 13:03

        А что за SDF?


        1. Leopotam
          28.02.2019 13:12
          +1

          Signed distance field, где в грейскейле пишется дистанция от контура и потом в шейдере решается как рисовать — сейчас почти во всех движках текст рисуется так. Даже на хабре были статьи


          1. al_sh
            28.02.2019 14:17

            Именно так. В Qt 5.12 SDF поддерживается нативно, даже утилитка есть готовящая атласы из ttf


    1. xverizex Автор
      28.02.2019 12:55

      Хотя можно еще цвет залить скорее всего через uniform переменные. Но я так не пробывал.


  1. SDraw
    28.02.2019 14:55

    Придраться есть к чему.
    — Избыточное использование glGetUniformLocation
    — Странное включение и отключение аттрибутов буфера при отрисовке, что, обычно, не делают каждый кадр
    — FT_Library может быть одна, а FT_Face на каждый Font свой
    — Фиксированный текст