Итак, начнем.
Первое, что я хотел бы здесь написать, так это шейдер, который я выписал из книги. Он накладывает двумерную текстуру на несколько треугольников.
За создание шейдеров у меня отвечает отдельный класс. Я пишу ему какой шейдер скомпилировать и он мне возвращает программу. Также он добавляет в 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 );
Комментарии (15)
Leopotam
28.02.2019 09:41А раньше был запрещен репост с других ресурсов. Делаем из хабра копипасту, но зачем...
xverizex Автор
28.02.2019 10:47А это не репост. Это я на форуме отписался о проделанной работе. А здесь я статью написал.
al_sh
28.02.2019 12:02+1т.е. статья о том, как поместить растр отданный фритэпом в опенгл текстуру? А зачем в RGB, почему не писать в альфу и не красить в шейдере? Почему не в атлас?
xverizex Автор
28.02.2019 12:21-1В rgba, чтобы цвет какой захочешь можно было установить. Там же параметры есть для rgb. Через шейдер статичный цвет. Как в атлас не пойму?
Leopotam
28.02.2019 12:28А зачем в RGB
Потому что автор не в курсе про SDF и как это потом пригодится.
и не красить в шейдере
И про заливку цвета через вертекс-колор, наверное, тоже.xverizex Автор
28.02.2019 12:56В android скорее всего это не прокатит. Я знаю про glVertexColorPointer и glVertexPointer и glTexCoordPointer. Но это без шейдеров.
Leopotam
28.02.2019 13:14Ну fixed pipeline давно эмулируется через встроенные шейдеры (а ниже gles2 сейчас живых девайсов вроде как нет), поэтому использовать его смысла нет. Льем цвета в вертексы и дальше пробрасываем из вертексного в фрагментный через интерполятор. Так делается везде, где требуется менять цвет глифов + всякие эффекты типа градиента.
xverizex Автор
28.02.2019 13:03А что за SDF?
Leopotam
28.02.2019 13:12+1Signed distance field, где в грейскейле пишется дистанция от контура и потом в шейдере решается как рисовать — сейчас почти во всех движках текст рисуется так. Даже на хабре были статьи
al_sh
28.02.2019 14:17Именно так. В Qt 5.12 SDF поддерживается нативно, даже утилитка есть готовящая атласы из ttf
xverizex Автор
28.02.2019 12:55Хотя можно еще цвет залить скорее всего через uniform переменные. Но я так не пробывал.
SDraw
28.02.2019 14:55Придраться есть к чему.
— Избыточное использование glGetUniformLocation
— Странное включение и отключение аттрибутов буфера при отрисовке, что, обычно, не делают каждый кадр
— FT_Library может быть одна, а FT_Face на каждый Font свой
— Фиксированный текст
andreili
Все просто, пока не возникает нужда во всяких там обводках и прочем ;)
Вот мой пример реализации из игры: bitbucket.org/schooldaysteam/nice-boat/src/master/nice_boat/src/OpenGLFont.cpp