Найти тему
ZDG

OpenGL #2: Готовим данные и рисуем простой треугольник

Оглавление

Приготовив OpenGL-контекст, займёмся подготовкой данных для рендеринга.

Предыдущие части: Приступим, помолясь, OpenGL на пальцах

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

Как мы уже обсудили, OpenGL использует нормализованные координаты (-1..1). Кроме того, ось Y направлена не вниз, как обычно, а вверх, как принято в математике.

Начало координат находится в центре экрана.

Задание вершин треугольника

Мы зададим треугольник с помощью массива из 9 вещественных чисел:

-2

В этом массиве содержатся три вершины треугольника, каждая вершина это три числа: координаты X, Y и Z. Координата Z сейчас просто равна нулю, то есть треугольник находится целиком в плоскости экрана.

Получение буфера под треугольник

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

  1. Чтобы поместить наш массив в память графической карты, нужно сначала в этой памяти выделить буфер.
  2. Напрямую мы это сделать не можем. Рассматривайте OpenGL как потусторонний мир, с которым мы можем общаться через функции-посредники.
  3. С помощью одной из таких функций мы просим OpenGL зарезервировать для нас номер буфера (handle). Мы не можем получить прямой адрес этого буфера, и довольствуемся выданным нам "номерком".

Можно в одном вызове функции запросить сразу несколько "номерков". Но нам пока требуется только один:

-3

Мы создали переменную, где будет храниться номерок буфера: vertex_buffer_handle. У неё тип GLuint, он совпадает с типом unsigned int. Суть такой подмены в том, что если вдруг в OpenGL что-то изменится, то тип GLuint может быть переопределён.

Далее мы вызываем функцию glGenBuffers() и передаём в неё параметры: 1 (нам нужен один буфер), и адрес переменной vertex_buffer_handle, в которую нужно сохранить номерок.

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

-4

Сначала мы вызываем функцию glBindBuffer(), которая связывает номерок буфера с определённой целью (target).

Мы передаём в glBindBuffer() константу GL_ARRAY_BUFFER. Это и есть одна из целей, которая использует буфер как массив общего назначения. Вторым параметром мы передаём ранее полученный номерок, чтобы OpenGL знал, какой буфер будет заполняться.

То есть, мы сообщили: буфер с номерком vertex_buffer_handle будет использоваться (с целью...) как массив.

Сразу после этого мы вызываем glBufferData(), куда передаём уже актуальные данные для выбранного буфера. Первым параметром указывается та же самая цель GL_ARRAY_BUFFER, затем общий размер нашего массива (это 9 чисел размера float), затем адрес массива vertices, и последний параметр GL_STATIC_DRAW – это подсказка (hint) для OpenGL, которая поясняет, как будут использоваться данные. В нашем случае мы поясняем, что эти данные не будут изменяться, и поэтому OpenGL может их как-нибудь оптимизировать.

Если вам кажется, что для такой простой операции, как передача массива, делается многовато действий, вы правы, но так оно работает.

Повторим ещё раз, как всё происходит:

  1. glGenBuffers(): Мы попросили у OpenGL создать буфер и получили номерок.
  2. glBindBuffer(): Мы привязали номерок буфера vertex_buffer_handle к цели GL_ARRAY_BUFFER, то есть этот буфер будет использоваться как массив.
  3. glBufferData(): Мы передали данные из своего массива vertices в цель GL_ARRAY_BUFFER. Так как к этой цели был ранее привязан номерок буфера vertex_buffer_handle, то переданные нами данные попадают именно в этот буфер.

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

Объекты массивов вершин

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

OpenGL для рисования использует "объекты массивов вершин" (vertex array objects, VAO). Почему это именно объекты, а не просто массивы? Объект это надстройка над массивом, которая содержит всю служебную информацию об этом массиве и его текущее состояние (не забываем, что всё это используется в потоках и конвейерах).

VAO-объекты находятся также на стороне OpenGL, поэтому применяем ранее использованный подход с "номерками":

-5

Заводим переменную-номерок vertex_array_handle и получаем этот самый номерок от glGenVertexArrays().

Обратите внимание, что ранее мы использовали glGenBuffers() для получения номерков буферов, а теперь glGenVertexArrays() для получения номерков VAO-объектов.

В каждый момент времени мы можем работать с одним объектом. С помощью glBindVertexArray() мы выбираем текущий объект – с номерком vertex_array_handle.

VAO-обект содержит настройки, которые связывают сырые данные в буфере с параметрами шейдера. То есть, если у шейдера допустим два параметра, то у первого параметра индекс 0, а у второго индекс 1. VAO-объект назначает этим индексам правильный формат данных.

С помощью glEnableVertexAttribArray(0) мы делаем доступным индекс 0 в VAO-объекте.

Повторим:

  1. Мы попросили у OpengGL номерок для объекта вершинного массива.
  2. Мы сделали этот объект текущим.
  3. Мы разрешили в нём использование индекса 0 для параметров шейдера.

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

-6

В функции glBindBuffer() мы привязываем номерок буфера vertex_buffer_handle к цели GL_ARRAY_BUFFER. Первый раз мы это делали, когда отправляли в буфер данные. Суть в том, чтобы сделать какой-то буфер текущим. Так как он и так уже текущий, это можно не делать, но оставим для порядка.

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

  1. 0 – индекс параметра в VAO-объекте. Ранее мы его разрешили для использования. То есть мы сейчас задаём структуру буфера для входного параметра шейдера с индексом 0.
  2. 3 – размер параметра шейдера. У вершины 3 координаты, поэтому в буфере каждые 3 элемента это одна вершина.
  3. GL_FLOAT – тип элементов, из которых состоит параметр (вещественное число).
  4. GL_FALSE – этот флаг отвечает за нормализацию координат. В нашем буфере они уже нормализованы, поэтому мы используем значение GL_FALSE, иначе можно использовать GL_TRUE.
  5. 0 – сколько всего байт занимает одна порция данных в буфере для этого параметра шейдера. В нашем случае одна порция это 3 числа типа GL_FLOAT. Так как вершины в буфере плотно упакованы по три элемента, можно указать 0 – это значит, что длина будет посчитана автоматически.
  6. NULL – смещение параметра шейдера от начала порции буфера. Так как вершины у нас стартуют прямо с самого начала порции, используем NULL.

Повторим:

Мы создали объект массива вершин, назначили ему индекс 0, разрешили его использовать, привязали к нему буфер с вершинами, и объяснили формат этого буфера.

Ура! Рисование треугольника!

Теперь у OpenGL есть все данные, чтобы рисовать:

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

-7

Сначала, как обычно, нужно назначить наш VAO-объект текущим с помощью glBindVertexArray().

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

Наконец, мы вызываем функцию glDrawArrays(), которая отрисует текущий выбранный VAO-объект. Рассмотрим её параметры по порядку:

  1. GL_TRIANGLES – говорит о том, что вершины в массиве нужно трактовать как геометрическую фигуру "треугольник"
  2. 0 – смещение от начала массива. Массив может содержать, допустим, 100500 треугольников, и необязательно рисовать их все. Можно начать с какого-то конкретного треугольника. Но у нас он пока только один, поэтому смещение 0.
  3. 3 – сколько вершин из массива использовать. Как и в случае со смещением, необязательно рисовать все треугольники до самого конца. Но в нашем массиве всего 3 вершины, и мы их все используем.

Эти две строчки мы размещаем в программном цикле после очистки экрана:

-8

И вот что получается:

-9

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

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

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

В следующем выпуске приступим к написанию шейдеров.

Текущий код лежит на github в ветке buffers.

Читайте дальше: Наш первый шейдер