Найти в Дзене
ZDG

OpenGL на пальцах

OpenGL (Open Graphics Library) это, вероятно, самая популярная библиотека для рисования трёхмерной графики. Существует огромный зоопарк 3D-движков, которые базируются на OpenGL.

Можно работать с чистым OpenGL, но это требует выполнения многих подготовительных пунктов. Примерно как собрать автомобиль. Автомобиль состоит из пары тысяч деталей. И даже если приложить к нему подробнейшую инструкцию с простейшими действиями, вы всё равно устанете, собирая его.

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

И хотя это существенно упрощает жизнь, основа никуда не девается. Это по-прежнему OpenGL, и как бы движки ни прятали всю начинку, всё равно она местами вылазит и ставит в тупик. Например, что такое шейдеры, как они работают?

Поэтому, даже если вам ничего не надо делать руками, всё равно нужно знать, как работает OpenGL.

Наша текущая задача – исключительно на пальцах, очень примерно, разобраться в этом. Подробности будут потом.

Зная основы, вы сможете подойти к изучению OpenGL более подготовленными, а также ориентироваться в любом движке и понимать код 3D-программ.

Приступим?

Процессор и графический процессор

В материале Растеризация треугольника использовался алгоритм, который выполняется центральным процессором компьютера. Он работает медленно, потому что процессор рисует, грубо говоря, одну точку за один раз, а точек надо нарисовать 100500.

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

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

Но перед тем, как нарисовать треугольник, требуется подготовить данные. И львиная доля работы с OpenGL заключается как раз в этой подготовке.

Передача данных

Центральный процессор и графический процессор никак друг с другом не общаются. Они находятся в разных мирах. Можно сказать, что это два отдельных компьютера, и у каждого есть своя оперативная память. Когда у нас есть данные для рисования треугольника (например, координаты трёх точек), то само собой, эти данные находятся в памяти нашей программы. А память видеокарты нашей программе недоступна.

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

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

Графический конвейер

Автомобили собираются на конвейере. На каждом этапе сборки выполняется какая-то одна операция. Сварка кузова, или прокладка проводов, или прикручивание дверей и т.д.

OpenGL тоже имеет конвейер обработки. В среде разработчиков его называют пайплайн (pipeline).

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

1. Преобразования координат

OpenGL – трёхмерная библиотека, и оперирует трёхмерными координатами. Даже если мы хотим нарисовать двумерный треугольник, мы должны описать его как трёхмерный. В то же время экран у нас не трёхмерный, поэтому любые 3D-координаты должны превратиться в 2D на экране.

То есть: берутся 3D-точки и проецируются на плоскость экрана. В проекции участвует перспективное преобразование. В материале Параллакс в играх: движение сквозь звёзды мы занимались именно тем, что проецировали 3D-звёзды на плоскость.

Кроме того, на этом этапе совершаются другие преобразования. OpenGL оперирует "нормализованными координатами устройства" (Normalized Device Coordinates), которые всегда расположены между -1 и 1.

-2

Если наше графическое окно размером 640*480, то диапазон x-координаты (0,639) должен быть преобразован в диапазон (-1,1), и диапазон у-координаты (0,479) аналогично в (-1,1).

Так как некоторые части графики могут не помещаться в окно, то всё, что выходит за пределы (-1,1), на этом шаге отбрасывается.

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

2. Построение геометрических примитивов

Так как мы передали всего лишь три точки, OpenGL не знает, что с ними делать. Их надо нарисовать как три точки? Или как две линии? Или как три линии? Или как закрашенный треугольник?

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

3. Растеризация

Каждый геометрический примитив, полученный на предыдущем этапе, переводится в растр. То есть в набор пикселов. Нет, мы пока не видим их на экране, но они уже в экранном пространстве, и на данный момент мы уже не имеем дела ни с какой геометрией, векторами и т.д., а только с дискретными пикселами.

-3

Этот этап соответствует тому, что описано в материале Растеризация треугольника.

4. Шейдинг

У нас уже есть все пикселы, которые надо нарисовать. Но видеокарта не знает, какого они должны быть цвета.

Назначение пикселу определенного цвета – это шейдинг (от слова shade – оттенок).

И вот в этом месте мы должны взять на себя управление и задать цвет пикселов. Потому что это можем решить только мы.

Для этого нужно написать шейдер. Подробнее чуть ниже, а пока перейдём к последнему этапу.

5. Блендинг и маскирование

Когда цвет пиксела уже ясен, остаётся его только нарисовать, но это не финальный цвет. Может использоваться операция смешивания (блендинга) нашего цвета и цвета, который сейчас на экране, что в результате даст новый цвет. Это может быть эффект прозрачности, но не только. Цвета можно менять по любым заданным математикой правилам. Также могут использоваться маски, которые определяют области, где может рисоваться пиксел, а где не может.

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

И вот наконец, цветные пикселы появляются на экране.

Шейдер

Вышеописанные этапы можно сгруппировать в два общих этапа:

  • Обработка координат
  • Обработка пикселов

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

Шейдер – это программа, которая изначально занималась раскрашиванием (шейдингом) пикселов. Отсюда и её название. Но сейчас шейдеры выполняют и другие задачи.

Постойте, я сказал "программа"? Да. Это программа, которая исполняется на микроядре графической карты. Из этого следует, что:

  • Шейдер не является частью нашей программы
  • Шейдер не работает на нашем процессоре

Как тогда вообще работать с ними?

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

По синтаксису он похож на C и имеет развитые возможности для работы с векторами и матрицами.

Когда шейдер написан, его нужно скомпилировать, то есть превратить из исходного кода в машинные инструкции. Это делается на лету прямо в нашей основной программе. Мы берём исходный код шейдера в виде обычной строки, передаём его в OpenGL-функцию для компиляции, и эта функция компилирует шейдер. Типа того:

shader = "int a=1; int b=2; int c=a+b";
compiledShader = OpenGL_CompileShader(shader);

Затем этот скомпилированный шейдер мы подключаем к конвейеру с помощью других OpenGL-функций. В результате:

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

Пиксельный шейдер

Давайте начнём с шейдера, который красит пикселы. Он также называется фрагментным (fragment shader). Фрагментом является один пиксел.

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

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

У шейдера есть входные и выходные параметры, которые объявляются специальным образом.

На входе мы получим, допустим, координаты пиксела, а выходным параметром будет цвет. Какой это должен быть цвет – решать нам.

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

Тогда нарисованный треугольник будет просто зелёным.

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

Вершинный шейдер

Это другой тип шейдера, который занимается обработкой не пикселов, а координат, и также называется вертекс-шейдером (vertex – вершина многоугольника). В конвейере он ставится на этап преобразования координат, то есть перед пиксельным шейдером.

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

Задача вершинного шейдера – подготовить координаты для дальнейшей работы, например, нормализовать их. Но требуется это не всегда. Допустим, мы послали на видеокарту уже нормализованные координаты, тогда в них ничего менять не нужно. Или мы можем послать 2D-координаты, а шейдер преобразует их в 3D.

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

Заключение

Итак, чтобы нарисовать треугольник через OpenGL, нам нужно

  • Написать вершинный шейдер
  • Написать пиксельный шейдер

Работающими примерами мы займёмся в следующих выпусках.

Читайте дальше: Приступим, помолясь