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

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

Существуют три основных подхода:

Готовые текстовые битмапы

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

Плюсы:

  • Максимальная производительность
  • Полная свобода дизайна (любые стили и эффекты)
  • Всю работу делает художник/дизайнер

Минусы:

  • Медленная локализация
  • Большой расход памяти
  • Нельзя динамически менять текст

Текстура с символами (текстурный атлас)

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

Плюсы:

  • Относительно высокая производительность
  • Быстрая локализация (сгенерировать новую текстуру для языка)
  • Шрифт в любом стиле
  • Динамическое изменение надписи

Минусы:

  • Необходимо написать генератор этих текстур
  • Для разных языков нужны разные атласы
  • Локализация увеличивает размер приложения

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

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

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

Плюсы:

  • Минимальный расход памяти (один файл шрифта)
  • Простая локализация
  • Полная динамичность текста
  • Поддержка всех языков

Минусы:

  • Требует больше CPU при первом использовании символов
  • Ограниченный стиль (зависит от шрифта)
  • Сложная реализация

Подробнее о работе с 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: экранные координаты

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

Текстурные координаты задаются от левого нижнего угла. Оси текстурных координат называются: (S, T).
Горизонтальная ось S направлена слева направо. Вертикальная ось 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 гораздо быстрее, обычной, но мои эксперименты особого выигрыша не выявили.

Личный блог Евгения Жирнова