Приготовив OpenGL-контекст, займёмся подготовкой данных для рендеринга.
Предыдущие части: Приступим, помолясь, OpenGL на пальцах
У нас для начала будет один треугольник с координатами, заданными в несколько непривычной системе:
Как мы уже обсудили, OpenGL использует нормализованные координаты (-1..1). Кроме того, ось Y направлена не вниз, как обычно, а вверх, как принято в математике.
Начало координат находится в центре экрана.
Задание вершин треугольника
Мы зададим треугольник с помощью массива из 9 вещественных чисел:
В этом массиве содержатся три вершины треугольника, каждая вершина это три числа: координаты X, Y и Z. Координата Z сейчас просто равна нулю, то есть треугольник находится целиком в плоскости экрана.
Получение буфера под треугольник
Массив вершин треугольника создан в памяти нашей программы, но OpenGL о нём ничего не знает. Нужно его перебросить на сторону видеокарты. Обратите внимание на последовательность действий.
- Чтобы поместить наш массив в память графической карты, нужно сначала в этой памяти выделить буфер.
- Напрямую мы это сделать не можем. Рассматривайте OpenGL как потусторонний мир, с которым мы можем общаться через функции-посредники.
- С помощью одной из таких функций мы просим OpenGL зарезервировать для нас номер буфера (handle). Мы не можем получить прямой адрес этого буфера, и довольствуемся выданным нам "номерком".
Можно в одном вызове функции запросить сразу несколько "номерков". Но нам пока требуется только один:
Мы создали переменную, где будет храниться номерок буфера: vertex_buffer_handle. У неё тип GLuint, он совпадает с типом unsigned int. Суть такой подмены в том, что если вдруг в OpenGL что-то изменится, то тип GLuint может быть переопределён.
Далее мы вызываем функцию glGenBuffers() и передаём в неё параметры: 1 (нам нужен один буфер), и адрес переменной vertex_buffer_handle, в которую нужно сохранить номерок.
Далее мы заполняем этот буфер на стороне OpenGL нашими данными о вершинах треугольника.
Сначала мы вызываем функцию glBindBuffer(), которая связывает номерок буфера с определённой целью (target).
Мы передаём в glBindBuffer() константу GL_ARRAY_BUFFER. Это и есть одна из целей, которая использует буфер как массив общего назначения. Вторым параметром мы передаём ранее полученный номерок, чтобы OpenGL знал, какой буфер будет заполняться.
То есть, мы сообщили: буфер с номерком vertex_buffer_handle будет использоваться (с целью...) как массив.
Сразу после этого мы вызываем glBufferData(), куда передаём уже актуальные данные для выбранного буфера. Первым параметром указывается та же самая цель GL_ARRAY_BUFFER, затем общий размер нашего массива (это 9 чисел размера float), затем адрес массива vertices, и последний параметр GL_STATIC_DRAW – это подсказка (hint) для OpenGL, которая поясняет, как будут использоваться данные. В нашем случае мы поясняем, что эти данные не будут изменяться, и поэтому OpenGL может их как-нибудь оптимизировать.
Если вам кажется, что для такой простой операции, как передача массива, делается многовато действий, вы правы, но так оно работает.
Повторим ещё раз, как всё происходит:
- glGenBuffers(): Мы попросили у OpenGL создать буфер и получили номерок.
- glBindBuffer(): Мы привязали номерок буфера vertex_buffer_handle к цели GL_ARRAY_BUFFER, то есть этот буфер будет использоваться как массив.
- glBufferData(): Мы передали данные из своего массива vertices в цель GL_ARRAY_BUFFER. Так как к этой цели был ранее привязан номерок буфера vertex_buffer_handle, то переданные нами данные попадают именно в этот буфер.
Впереди нас ждёт ещё больше нудятины, так что не расслабляйтесь. Главное – чётко понимать, какой этап нужен для чего.
Объекты массивов вершин
На данный момент мы добились того, что на стороне OpenGL находится буфер, заполненный нашими данными. Но буферы – это просто области памяти, и OpenGL ничего не знает об их структуре. Наши следующие шаги – объяснить OpenGL, что это за данные.
OpenGL для рисования использует "объекты массивов вершин" (vertex array objects, VAO). Почему это именно объекты, а не просто массивы? Объект это надстройка над массивом, которая содержит всю служебную информацию об этом массиве и его текущее состояние (не забываем, что всё это используется в потоках и конвейерах).
VAO-объекты находятся также на стороне OpenGL, поэтому применяем ранее использованный подход с "номерками":
Заводим переменную-номерок vertex_array_handle и получаем этот самый номерок от glGenVertexArrays().
Обратите внимание, что ранее мы использовали glGenBuffers() для получения номерков буферов, а теперь glGenVertexArrays() для получения номерков VAO-объектов.
В каждый момент времени мы можем работать с одним объектом. С помощью glBindVertexArray() мы выбираем текущий объект – с номерком vertex_array_handle.
VAO-обект содержит настройки, которые связывают сырые данные в буфере с параметрами шейдера. То есть, если у шейдера допустим два параметра, то у первого параметра индекс 0, а у второго индекс 1. VAO-объект назначает этим индексам правильный формат данных.
С помощью glEnableVertexAttribArray(0) мы делаем доступным индекс 0 в VAO-объекте.
Повторим:
- Мы попросили у OpengGL номерок для объекта вершинного массива.
- Мы сделали этот объект текущим.
- Мы разрешили в нём использование индекса 0 для параметров шейдера.
Теперь нужно связать этот индекс с буфером и пояснить, как именно нужно читать данные из буфера.
В функции glBindBuffer() мы привязываем номерок буфера vertex_buffer_handle к цели GL_ARRAY_BUFFER. Первый раз мы это делали, когда отправляли в буфер данные. Суть в том, чтобы сделать какой-то буфер текущим. Так как он и так уже текущий, это можно не делать, но оставим для порядка.
Далее вызываем функцию glVertexAttribPointer(), которая задаёт (там, внутри OpenGL) указатель на текущий буфер и описывает его структуру, а именно, порциями какой длины будут читаться параметры шейдера из буфера. Посмотрим на параметры функции по порядку:
- 0 – индекс параметра в VAO-объекте. Ранее мы его разрешили для использования. То есть мы сейчас задаём структуру буфера для входного параметра шейдера с индексом 0.
- 3 – размер параметра шейдера. У вершины 3 координаты, поэтому в буфере каждые 3 элемента это одна вершина.
- GL_FLOAT – тип элементов, из которых состоит параметр (вещественное число).
- GL_FALSE – этот флаг отвечает за нормализацию координат. В нашем буфере они уже нормализованы, поэтому мы используем значение GL_FALSE, иначе можно использовать GL_TRUE.
- 0 – сколько всего байт занимает одна порция данных в буфере для этого параметра шейдера. В нашем случае одна порция это 3 числа типа GL_FLOAT. Так как вершины в буфере плотно упакованы по три элемента, можно указать 0 – это значит, что длина будет посчитана автоматически.
- NULL – смещение параметра шейдера от начала порции буфера. Так как вершины у нас стартуют прямо с самого начала порции, используем NULL.
Повторим:
Мы создали объект массива вершин, назначили ему индекс 0, разрешили его использовать, привязали к нему буфер с вершинами, и объяснили формат этого буфера.
Ура! Рисование треугольника!
Теперь у OpenGL есть все данные, чтобы рисовать:
Он возьмет разрешенный для рисования объект массива вершин (с индексом 0), прочитает из него координаты вершин (объект транслирует эти координаты из привязанного к нему буфера), и выведет на экран треугольник.
Сначала, как обычно, нужно назначить наш VAO-объект текущим с помощью glBindVertexArray().
Замечу, что до этого мы уже делали его текущим, поэтому второй раз вызывать эту функцию необязательно. Но я оставил её для тренировки, потому что в дальнейшем у нас может быть несколько VAO-объектов и нужно будет делать текущими разные.
Наконец, мы вызываем функцию glDrawArrays(), которая отрисует текущий выбранный VAO-объект. Рассмотрим её параметры по порядку:
- GL_TRIANGLES – говорит о том, что вершины в массиве нужно трактовать как геометрическую фигуру "треугольник"
- 0 – смещение от начала массива. Массив может содержать, допустим, 100500 треугольников, и необязательно рисовать их все. Можно начать с какого-то конкретного треугольника. Но у нас он пока только один, поэтому смещение 0.
- 3 – сколько вершин из массива использовать. Как и в случае со смещением, необязательно рисовать все треугольники до самого конца. Но в нашем массиве всего 3 вершины, и мы их все используем.
Эти две строчки мы размещаем в программном цикле после очистки экрана:
И вот что получается:
Очистка экрана делается тёмно-красным цветом, а затем рисуется белый треугольник. Почему белый?
Это работает стандартный шейдер, который окрашивает все пикселы треугольника в белый цвет. Мы пока ещё не писали ни вершинного, ни фрагментного шейдера, и честно говоря, без них я не был уверен в результате. Но всё получилось.
Мы вывели на экран треугольник, и это уже неплохое достижение. Теперь нужно, как говорят англоязычные граждане, "обернуть свой ум вокруг этого", то есть ещё раз вдумчиво пройтись по всем пунктам и понять, как они работают.
В следующем выпуске приступим к написанию шейдеров.
Текущий код лежит на github в ветке buffers.
Читайте дальше: Наш первый шейдер