Pull to refresh

Основы работы с OpenGL ES 2.0 на iPhone 3G S

Reading time 7 min
Views 17K
Одно из самых приятных нововведений в iPhone 3GS — более быстрая и мощная графическая платформа с поддержкой OpenGL ES 2.0. К сожалению, информации от Apple о том, как именно задействовать открывшиеся возможности, крайне мало. Практически для всех API у них есть отличная документация с образцами кодов, но проблема в том, что в случае с OpenGL примеры всегда оставляли, мягко говоря, желать лучшего.

Более того, начинающим работу с OpenGL ES 2.0 не предлагается ни базовых примеров, ни шаблона XCode. Чтобы воспользоваться расширенными графическими возможностями, придется осваивать их самостоятельно. Не стоит ошибочно полагать, что OpenGL ES 2.0 — незначительно доработанная версия OpenGL ES 1.1 с парочкой новых функций. Отличия между ними кардинальные! Конвейер с фиксированными функциями исчез, и теперь для отображения на экране обычного треугольника понадобится более глубокое знакомство с основами компьютерной графики, включая шейдеры.

Учитывая полное отсутствие документации, я решил создать самое простое приложение на iPhone с помощью OpenGL ES 2.0. Для пользователей оно вполне может стать отправной точкой при создании приложений. В качестве вариантов я рассматривал вращающийся чайник и прочие конструкции, но в итоге решил не вдаваться в детали по загрузке модели, а просто обновить приложение OpenGL ES 1.1, являющееся частью шаблона XCode. Полный итоговый код можно загрузить здесь.

Ничего интригующего — просто вращающийся квадрат. Впрочем, этого достаточно, чтобы познакомиться с основами запуска OpenGL, создания шейдеров, а также подключения их к программе с последующим использованием. Более того, в конце мы рассмотрим функцию, невозможную в OpenGL ES 1.1. Готовы?

Инициализация


Инициализация OpenGL практически ничем не отличается от OpenGL ES 1.1. Единственная разница в том, что нужно будет сообщить о новой версии API для ES 2.0.

context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

Все остальное, включая EAGLView и создание так называемых задних буферов (back buffers), осталось таким, как раньше — останавливаться на этих моментах я не буду.

Не забывайте, что при инициализации OpenGL ES 2.0 не будет возможности вызывать относящиеся к OpenGL ES 1.1 функции. Попытка работать с ними приведет к сбою программы, поскольку для этих функций отсутствуют корректные настройки. Соответственно, при желании воспользоваться преимуществами графики 3GS, одновременно обеспечив совместимость с предыдущими моделями, в процессе работы нужно будет проверять тип устройства, активируя OpenGL ES 1.1 или 2.0 — для каждого варианта будут предусмотрены собственные коды.

Создание шейдеров


Для рендеринга многоугольников OpenGL ES 1.1 использует конвейер с фиксированными функциями. Чтобы визуализировать объект на экране в OpenGL ES 2.0, необходимо прописать шейдеры — мини-программы, созданные под конкрентную графическую платформу. Их задача — трансформировать входные данные (вершины и состояния) в изображение на экране. Пишутся шейдеры на языке OpenGL Shader Language (сокращенно GLSL), который не составит ни малейших проблем для тех, кто привык работать с C. Безусловно, чтобы пользоваться всеми его возможностями, нужно будет изучить некоторые детали. Ниже будут представлены лишь основные концепции и их взаимосвязи.

Шейдеры бывают двух типов: вершинные шейдеры выполняются для каждой вершины, фрагментные — для каждого пикселя. Технически, вторые выполняются для каждого фрагмента, что может не соответствовать пикселю, например, при сглаживании. Пока можно смело считать, что фрагментные шейдеры выполняются для каждого визуализируемого пикселя.

Вершинный шейдер вычисляет позицию вершины в усеченном пространстве (Clip Space). Опционально могут рассчитываться и другие значения для последующего использования фрагментным шейдером.
  • Вершинный шейдер принимает два типа данных — унифицированные и атрибуты. Унифицированные вводные — это значения, задающиеся единожды из основной программы и применяемые ко всем вершинам, обрабатываемым вершинным шейдером при запросе рисования. Так, например, трансформация представления "world view" является унифицированной вводной.

  • Вводные атрибуты одного запроса рисования могут варьироваться для каждой вершины. Положение, нормаль, информация по цвету — все это вводные атрибуты.

На выходе вершинный шейдер обеспечивает два типа данных:
  • Подразумеваемая позиция в переменной "gl_Position", т.е. положение вершины в усеченном пространстве (позже трансформируется в область просмотра Viewport Space).

  • Варьирующиеся результаты. Переменные, определяемые варьирующимся атрибутом, будут интерполироваться между вершинами, а полученные значения — передаваться в качестве вводных данных фрагментным шейдерам.

В типовой программе вершинный шейдер просто трансформирует позицию вершины из пространства модели (Model Space) в усеченное пространство и передает цвет вершины для интерполяции фрагментным шейдером.

uniform mat4 u_mvpMatrix;

attribute vec4 a_position;
attribute vec4 a_color;

varying vec4 v_color;

void main()
{
gl_Position = u_mvpMatrix * a_position;
v_color = a_color;
}


Фрагментные шейдеры рассчитывают цвет фрагмента (пикселя). Вводными параметрами для них являются варьирующиеся переменные, сгенерированные вершинным шейдером в дополнение к переменной "gl_Position". Расчет цвета может ограничиваться добавлением константы в "gl_Position", а может представлять собой поиск пикселя текстуры по uv координатам или сложную операцию, принимающую в расчет условия освещения.

Наш фрагментный шейдер будет элементарным: принимая цвет от вершинного шейдера, он применяет его к данному фрагменту.

varying vec4 v_color;

void main()
{
gl_FragColor = v_color;
}


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

Компилирование шейдеров


У нас есть несколько готовых шейдеров. Как заставить их работать? Понадобится несколько шагов, первым из которых станет компиляция и установка связей.

В процессе работы необходимо загрузить текст исходного кода для каждой вершины вместе с фрагментными шейдерами, после чего выполнить компиляцию посредством пары обращений к OpenGL.

const unsigned int shader = glCreateShader(type);
glShaderSource(shader, 1, (const GLchar**)&source, NULL);
glCompileShader(shader);


Как только компляция для шейдеров будет выполнена, нужно создать программу шейдеров, добавить сами шейдеры и связать их между собой.

m_shaderProgram = glCreateProgram();
glAttachShader(m_shaderProgram, vertexShader);
glAttachShader(m_shaderProgram, fragmentShader);
glLinkProgram(m_shaderProgram);


Установка связей подразумевает переопределение данных вершинного шейдера на выходе на предполагаемый результат фрагментного шейдера.

Возможна диагностика ошибок при компиляции и установке связей с выводом на экран сообщения о причине ошибки.

int success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (success == 0)
glGetShaderInfoLog(shader, sizeof(errorMsg), NULL, errorMsg);


Если идея компиляции и связывания программ в процессе работы вам не слишком нравится, спешу заметить, что это мнение разделяют многие. В идеальном варианте данный шаг должен выполняться в автономном режиме, по аналогии с компилированием исходного кода для основной программы в Objective C. К сожалению, приоритетом для Apple являются открытость и возможность изменения формата в будущем, поэтому нас вынуждают выполнять компиляцию с установкой связи «без отрыва от производства». Это не просто раздражает, но и, теоретически, подразумевает достаточно низкую скорость при наличии нескольких шейдеров. Никому не хочется терять лишние секунды при запуске приложения, но пока приходится принимать это как данность в обмен на возможность работы с шейдерами на iPhone.

Привязка


Мы уже практически готовы к использованию шейдеров, но прежде необходимо оговорить, как корректно настраивать вводные данные. Вершинный шейдер рассчитывает на настроенную матрицу mvp (model-view-projection), а также поток данных вершин с позициями и цветами.

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

m_a_positionHandle = glGetAttribLocation(m_shaderProgram, "a_position");
m_a_colorHandle = glGetAttribLocation(m_shaderProgram, "a_color");
m_u_mvpHandle = glGetUniformLocation(m_shaderProgram, "u_mvpMatrix");


Работа с шейдерами


И вот, наконец, с помощью наших шейдеров можно выполнить рендиринг нескольких многоугольников. Все, что для этого нужно, — запустить шейдерную программу…

glUseProgram(m_shaderProgram);


… и настроить корректные вводные, задействовав для ввода параметров запрошенные ранее дескрипторы:

glVertexAttribPointer(m_a_positionHandle, 2, GL_FLOAT, GL_FALSE, 0, squareVertices);
glEnableVertexAttribArray(m_a_positionHandle);
glVertexAttribPointer(m_a_colorHandle, 4, GL_FLOAT, GL_FALSE, 0, squareColors);
glEnableVertexAttribArray(m_a_colorHandle);
glUniformMatrix4fv(m_u_mvpHandle, 1, GL_FALSE, (GLfloat*)&mvp.m[0] );


Теперь мы просто вызываем любую из функций рендеринга, известных по OpenGL ES 1.1 ("glDrawArrays" или "glDrawElements"):

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);


Если все прошло нормально, на экране появится корректно визуализированная модель. В нашем случае это просто квадрат, который станет основой для рендеринга собственных моделей — с нужными трансформациями, шейдерами и эффектами.

И небольшой бонус напоследок


Понимаю, что не слишком заманчиво проделать все это лишь для того, чтобы получить тот же вращающийся квадрат, что и в шаблоне OpenGL ES 1.1. Безусловно, это нужный и важный первый шаг, но все же… Поэтому, чтобы продемонстрировать, насколько легко с GLSL создавать различные эффекты, представляю модифицированный пиксельный шейдер.

float odd = floor(mod(gl_FragCoord.y, 2.0));
gl_FragColor = vec4(v_color.x, v_color.y, v_color.z, odd);


Данная версия фрагментного шейдера проверяет, является ли пиксель четной или нечетной строкой, и визуализирует четные строки как полностью прозрачные, создавая на экране эффект полос. Добиться подобного результата в OpenGL ES 1.1 было весьма проблематично, а в версии 2.0 это всего пара простых строк.

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

Исходный код к уроки можно скачать здесь.
Текст оригинальной статьи на английском языке здесь.
Tags:
Hubs:
+45
Comments 28
Comments Comments 28

Articles