Выводим текст в OpenGL ES 2.0

Перед каждый разработчиком, если он пишет приложение графическое на своём движке, рано или поздно встаёт вопрос: каким образом вывести текст. Я напомню, что OpenGL(ES) умеет только рисовать треугольники и накладывать на них текстуру.

Таким образом, учитывая доступные средства, вывести текст можно несколькими способами:

  • статическая текстура с готовыми надписями и текстурные координаты каждой надписи (два треугольника на надпись)
  • статическая текстура с символами и текстурные координаты каждого символа (два треугольника на символ)
  • динамическая текстура с символами и файл шрифта (два треугольника на символ)

Статическая текстура с готовыми надписями и текстурные координаты каждой надписи

Все надписи, которые встречаются в игре, рисуются заранее в текстуру. Такой вариант самый быстрый и простой в реализации для программиста. Фактически, вам надо взять текстуру и ее нарисовать (надеюсь вы это уже умеете).
Раскрывает полный простор для творчества, можно рисовать любую надпись, любым стилем и цветом.
Для оптимизации можно все надписи поместить в одну текстуру и рисовать все надписи на экране в один проход. При локализации необходимо рисовать другую текстуру с переведенным текстом.

Плюс/минусы:
+ самый быстрый вариант по скорости работы
+ всю работу за вас делает художник
+ надпись в любом стиле
− медленная локализация
− тратит много памяти
− статическая надпись

Статическая текстура с символами и текстурные координаты каждого символа

Для реализации этого способа необходимо каким-то образом сгенерировать текстурный атлас с каждым символом из шрифта.
Самый медленный в реализации способ из предложенных, зато каждый символ может быть нарисован в любом стиле. Но помните, что для каждого языка придётся заново рисовать каждый символ в выбранном вами стиле.

Плюсы/минусы:
+ относительно быстрая локализация (сгенерировать новую текстуру для языка)
+ шрифт в любом стиле
+ динамическое изменение надписи
− необходимо написать генератор этих текстур
− локализация увеличивает размер приложения

Динамическая текстура и файл шрифта

Фактически этот вариант представляет собой второй способ, но вместо текстуры с символами, используется файл шрифта (например ttf), где все эти символы описаны в векторном формате. В этом случае вместо мегабайтных текстур вам необходимо хранить один файл шрифта размером в десятки, максимум сотни, килобайт.

Текстуру можно создавать двумя способами: заполнить её всеми используемыми символами (26 — латинский алфавит, 33 — кириллица) или добавлять символ только тогда, когда он понадобится. Уже сложившейся традицией для генерации символа из шрифта является использование библиотеки FreeType.

Плюсы/минусы:
+ самая быстрая локализация
+ динамическое изменение надписи
+ самый маленький размер
− шрифт в одном стиле

Про работу с FreeType (+пример) можно почитать в моем следующем посте: «Использование библиотеки FreeType для растеризации символов».

Краткая информация по OpenGL ES 2.0

Всем привет! Решил систематизировать свои знания. Просьба относится к тексту ниже, как к вольному сочинению на тему «OpenGLES». Спасибо за понимание!

Вершинные координаты

В OpenGL используется правосторонняя система координат. Обычно направления координат X, Y и Z проверяются на пальцах правой руки. Большой палец — это X, указательный — Y, а средний — Z. По умолчанию X направлен вправо, Y вверх, а Z смотрит на нас.
В двумерном пространстве координата X обычно направлена вправо, Y — вниз, а Z игнорируют. Центр системы координат слева сверху, но иногда центр совпадает с центром экрана.

OpenGL: мировые координаты

Экранные координаты

Координаты экрана представляют собой две оси: горизонтальная и вертикальная (X и Y соответственно).
Горизонтальная ось направлена вправо, вертикальная — вверх. Центр экрана — это начало координат.
Координаты углов экрана: левый верхний (-1.0, 1.0), левый нижний (-1.0, -1.0), правый нижний (1.0, -1.0), правый верхний (1.0, 1.0).

OpenGL: экранные координаты

Текстурные координаты

Текстурные координаты задаются от левого нижнего угла. Оси текстурных координат называются по-разному: (U, V) или (S, T).
Горизонтальная ось U или S направлена слева направо. Вертикальная ось V или T направлена вверх.
Для наложения текстуры один в один, координаты должны быть в пределах от нуля до единицы. Если задать координаты больше единицы, то в OpenGL можно задать несколько вариантов обработать такую ситуацию: повторение текстуры, зеркалирование, повторение крайних пикселей.

OpenGL: текстурные координаты

Немного о треугольниках

По умолчанию рисование треугольников идет против часовой стрелки. Существуют три способа задать вершины:

  • GL_TRIANGLES — по три вершины на треугольник
  • GL_TRIANGLE_STRIP — три вершины на треугольник, затем каждая следующая с двумя предыдущими
  • GL_TRIANGLE_FAN — представляет собой веер, где первая вершина является центральной точкой веера, а остальные добавляют треугольники к вееру, подробнее в википедии

Для создания прямоугольника с помощью GL_TRIANGLE_STRIP используется такой порядок вершин (при обходе против часовой стрелки): левый нижний, правый нижний, левый верхний, правый верхний.

OpenGL:  координаты для рисования квадрата с помощью GL_TRIANGLE_STRIP

Шейдеры

Шейдер состоит из двух частей: вершинный шейдер и пиксельный шейдер.
На входе шейдер получает данные текущей вершины (например, позиция точки, цвет пикселя или текстурные координаты) и параметры общие для всего шейдера (матрица транформации, общая прозрачность объекта и так далее).
На выходе вершинный шейдер выдает координаты точки (с помощью gl_Position), а пиксельный — цвет точки (gl_FragColor).

OpenGL: работа шейдера
источник картинки

Данные текущей вершины называются аттрибутами и задаются с помощью ключевого слова "attribute" в самом шейдере, а передаются в него процедурой glVertexAttribPointer().

Общие параметры для всего шейдера задаются с помощью ключевого слова "uniform" и передача их идет процедурой glUniform().

Также есть ключевое слово "varying", им обозначаются параметры, которые передаются от вершинного шейдера к пиксельному. К этим параметрам применяется интерполяция от вершине к вершине, потому что вершин мало, а пикселей много. Поэтому вершинный шейдер вызывается реже, а пиксельный чаще.

Для установки параметров типа uniform используется такой код:

// Получаем ссылку на параметр "uniformName" в шейдере
GLuint uniformId = glGetUniformLocation(shader, "uniformName");
// Устанавливаем значение параметра
glUniform(uniformId, uniformValue);

Поскольку параметр типа uniform может представлять собой матрицу или массив 2-4 элементов, функция glUniform() имеет множество модификаций (glUniform1i, glUniform4f и так далее).

Параметры типа attribute задаются для каждой вершины. У каждого аттрибута необходимо знать две вещи: количество элементов в аттрибуте и тип элемента. Например, позиция вершины в 3D координатах XYZ имеет три элемента тип GL_FLOAT, а текстурные координаты UV имеют соответственно два элемента и тип GL_FLOAT.

Аттрибуты для вершин можно представить двумя способами.

В первом случае это просто массив для каждого типа аттрибутов:

// Позиция каждой вершины
float [] position = 
{ 
    x0, y0, z0, 
    x1, y1, z1, 
    ..., 
    xN, yN, zN 
};
// Текстурные координаты каждый вершины
float [] texCoord = 
{ 
    u0, v0, 
    u1, v1, 
    ..., 
    uN, vN 
};

Передача аттрибутов в шейдерв таком виде элементарна и не представляет сложности:

// Получаем ссылку на аттрибут "position" в шейдере
positionId = glGetAttribLocation(shader, "position");
// Передаем массив аттрибутов с тремя элементами типа float для каждой вершины
glVertexAttribPointer(positionId, 3, GL_FLOAT, GL_FALSE, 0, position);
// Включаем аттрибут
glEnableVertexAttribArray(positionId);
 
// Получаем ссылку на аттрибут "texCoord" в шейдере
texCoordId = glGetAttribLocation(shader, "texCoord");
// Передаем массив аттрибутов с двумя элементами типа float для каждой вершины
glVertexAttribPointer(texCoordId, 2, GL_FLOAT, GL_FALSE, 0, texCoord);
// Включаем аттрибут
glEnableVertexAttribArray(texCoordId);

Во втором случае все аттрибуты задаются в одном массиве и называется такой буфер interleaved:

// Данные для вершин
float [] vertices = 
{ 
    x0, y0, z0, u0, v0,
    x1, y1, z1, u1, v1,
    ...,
    xN, yN, zN, uN, vN
};

Код для передачи в шейдер похож на первый случай:

// Размер аттрибутов для одной вершины в байтах (5 элементов типа float)
#define VERTEX_SIZE (5 * sizeof(float))
// Начало 3D координат в буфере (координаты идут с самого начала буфера)
#define POS_OFFSET 0
// Начало текстурных координат в буфере (текстурные 
// координаты начинаются сразу после трех 3D координат
#define TEX_OFFSET 3
 
// Получаем ссылку на аттрибут "position" в шейдере
positionId = glGetAttribLocation(shader, "position");
 
// Передаем массив аттрибутов с тремя элементами типа float для каждой вершины
glVertexAttribPointer(
    positionId, 
    3, 
    GL_FLOAT, 
    GL_FALSE, 
    VERTEX_SIZE, 
    vertices + POS_OFFSET);
 
// Включаем аттрибут
glEnableVertexAttribArray(positionId);
 
// Получаем ссылку на аттрибут "texCoord" в шейдере
texCoordId = glGetAttribLocation(shader, "texCoord");
 
// Передаем массив аттрибутов с двумя элементами типа float для каждой вершины
glVertexAttribPointer(
    texCoordId, 
    2, 
    GL_FLOAT, 
    GL_FALSE, 
    VERTEX_SIZE, 
    vertices + UV_OFFSET);
 
// Включаем аттрибут
glEnableVertexAttribArray(texCoordId);

Как нетрудно заметить, во втором случае ссылка на данные фактически передается два раза. Умные люди придумали выход из этой ситуации и называется он VertexBufferObject. Вкратце идея такая: создаем один раз буфер, а затем в glVertexAttribPointer() передаем только смещения от начала.

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

Блог Евгения Жирнова