Учебник по 2D-графике (II): современный OpenGL - вершины, конвейеры и шейдеры

В другой статье я представил OpenGL и кратко рассказал, почему я выбрал Java, OpenGL и JOGL. Я также представил рабочий пример на Java, который был бы эквивалентом первой программы Hello World. В предполагаемом примере использовался старый синтаксис OpenGL, который легко понять, но не оптимизирован. В OpenGL это называется немедленным режимом, при котором команды рисования передаются непосредственно в графический процессор, заключенные в блоки glBegin и glEnd.

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

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

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

Помните, что это не исчерпывающий учебник, объясняющий каждую концепцию OpenGL, а собранный мной материал, объясняющий шаги, которые я нашел более сложными при изучении OpenGL. Основное внимание уделяется основным, но полным примерам, которые иллюстрируют, как использовать OpenGL для приложений визуализации 2D-данных. Не беспокойтесь, если вы не уверены в объеме определенных ключевых слов OpenGL, сосредоточившись на получении общего представления о том, как работают шейдеры и конвейер, будет намного проще изучить дополнительный материал. Примеры подготовлены также для того, чтобы заполнить пробелы, которые я обнаружил в наиболее рекомендуемых учебных материалах (к которым я вернусь в конце серии руководств), и чтобы выделить области, на решение которых мне потребовалось больше времени.

Конвейер и шейдеры

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

Таким образом, OpenGL преобразует наборы вершин в реальные визуализированные изображения и будет делать это с помощью так называемых шейдеров. Для 2D это то же самое, мы просто всегда рисуем в плоскости z = 0.

Говоря простым языком, это работает так:

  1. Приложение (например, Java или C ++) определит точки, которые будут использоваться для каждой формы / примитива, который нужно нарисовать. Это будет просто список вершин, поэтому, если вы хотите нарисовать только треугольник, для каждого примитива треугольника потребуются три вершины.
  2. Программа вершинного шейдера будет выполняться один раз для каждой вершины, и она будет определять расположение вершин и основную форму / примитив / объект, который нужно нарисовать. Вершинный шейдер предназначен для выполнения таких операций, как масштабирование и преобразование для каждой вершины.
  3. При желании геометрический шейдер преобразует вершины в формы. Таким образом можно было бы передать программе центр квадрата и позволить геометрическому шейдеру вывести четыре вершины квадрата.
  4. Программа фрагментного шейдера будет выполняться один раз для каждого отрисовываемого пикселя, и она определяет цвет пикселя.

Все шейдеры определены и скомпилированы (во время выполнения) для графического процессора видеокарты с использованием языка GLSL.

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

В этом примере мы будем использовать вершинный шейдер, геометрический шейдер и фрагментный шейдер. Идея состоит в том, что, учитывая набор координат (вершин) от приложения, наш графический процессор будет рисовать квадраты с центром в этих координатах и ​​раскрашивать их в соответствии с их расположением в окне. Это совершенно правильный пример, который прекрасно иллюстрирует, как программировать OpenGL с использованием «основного» режима.

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

Рисование пикселя с помощью шейдеров и GLSL

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

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

Наш вершинный шейдер ничего не делает, он просто передает точку вершины следующему шейдеру, но это все равно необходимо.

Разберем детально исходный код.

В первой строке указано, что мы используем ядро ​​OpenGL 3.3. Мы будем кодировать для 3.3, даже если - на момент написания этой статьи - OpenGL достиг версии 4.6. Причина этого заключается в том, что 3.3 является первой версией, обеспечивающей базовое программирование, и что, делая это, мы можем использовать старые графические карты.

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

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

Переменная типа vec2 определяет вектор с двумя числами с плавающей запятой. Поскольку мы имеем дело с 2D, нам нужны только две точки. При необходимости могут быть предоставлены дополнительные данные.

Второй блок - это основная функция. OpenGL во многом похож на синтаксис языка Си, поэтому шейдеры всегда имеют «главную» функцию. В этой функции мы определяем переменную gl_Position, которая имеет особое значение в OpenGL. Он определяет конечное положение вершины. В этом случае мы определяем вектор с 4 плавающими точками, который включает в себя трехмерные координаты плюс значение, используемое для проекции. В нашем случае координата Z равна 0, а значение проекции жестко запрограммировано на 1.

Это все, что нам нужно сделать в нашем вершинном шейдере, который превращает местоположение x / y в правильный вектор с 4 плавающими точками.

Геометрический шейдер

Следующий шейдер - шейдер геометрии. Этот шейдер может управлять вершинами и вычислять примитивы. Важно понимать, что графические процессоры манипулируют основными примитивами: точками, линиями и треугольниками. И все.

В качестве примера мы сначала предоставим геометрический шейдер, который просто генерирует 4 точки вокруг вершины:

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

gl_Position - это переменная OpenGL, используемая для определения позиции вершины. В этом случае мы определяем его четыре раза, по одному на каждую сгенерированную вершину. Обратите внимание, как мы получаем входное значение (gl_in [0]) и переводим его со смещением 0,2, чтобы построить каждую точку нашего воображаемого квадрата. Для каждой точки мы вызываем EmitVertex () и EndPrimitive (), поскольку мы генерируем 4 примитива по 1 вершине в каждом (точка по-прежнему является примитивом ). Позже мы сгенерируем четыре треугольника в каждом месте, чтобы дополнительно проиллюстрировать, как работают геометрические шейдеры.

Фрагментный шейдер

Теперь, когда у нас есть пиксель, определенный вершинным шейдером, мы закодируем наш фрагментный шейдер, который, опять же, представляет собой простую процедуру, которая просто рисует пиксель красным цветом (RGB 1,0,0). . Код выглядит следующим образом:

VBA и VBO

Прежде чем перейти к собственно коду реализации JOGL, давайте еще раз рассмотрим, что такое VAO и VBO в OpenGL. VBO (объект буфера вершин) - это буфер, хранящийся в памяти видеокарты, который содержит данные, которыми должны управлять шейдеры. Распределение и хранение на видеокарте происходит намного быстрее, чем перемещение данных назад и вперед от процессора. Идея состоит в том, что мы загружаем в эти буферы данные рендеринга, а буферы обрабатываются графическим процессором. Каждый VBO обычно содержит данные, относящиеся к объекту. VBO может содержать всю информацию, относящуюся к 3D-модели динозавра, в 2D, каждый VBO может, например, содержать вершины и необходимую информацию для рисования логического элемента или транзистора в программе САПР EDA (Electronic Design Automation). .

VAO - это надмножество VBO, а это значит, что нам нужен хотя бы один VAO. Одним из возможных примеров использования VAO может быть группировка всех визуализированных VBO определенного уровня игры в VAO. Таким образом, основной программе будет легче управлять тем, какие буферы необходимо загрузить. Это способ группировки буферов, которые могут потребоваться одновременно. Базовые 2D-приложения могут работать только с одним VAO. Для сложных приложений его использование может дать некоторые преимущества, особенно если вы не можете поместить всю информацию в память одновременно.

Класс OpenGL Java

Мы создадим новый класс с именем Window C, который будет содержать три шейдера. Мы определим один VAO и два VBO, каждый из которых будет содержать одну 2D-координату x, y. Мы обработаем оба VBO и в итоге получим два воображаемых квадрата, нарисованных на экране.

Также нам понадобится наш основной класс:

Результат, который мы получаем, как и ожидалось, представляет собой вершины двух мнимых квадратов, каждая из которых расположена вокруг (0,5,0,5) и (-0,5, -0,5).

Рисование треугольников вместо точек

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

Резюме

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

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

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