Уроки по OpenGL с сайта OGLDev


Урок 02 - Привет, точка!

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

В этом уроке мы впервые увидим использование буферов вершин (VBO). Как следует из названия, они используются для хранения вершин. Объекты, существующие в трехмерном мире, такие как монстры, замки или просто вращающийся куб всегда могут быть составлены из соединенных последовательно вершин. Буферы вершин наиболее эффективный способ для загрузки вершин в GPU. Они могут быть сохранены в видео памяти и обеспечить быстрейший доступ к GPU, поэтому они настоятельно рекомендуются.

Начиная с этого урока мы используем фиксированный конвейер функций вместо написания своего собственного. На самом деле, никаких трансформации в первых 2 уроках не будет. Мы просто считаем, что данные проходят через конвейер. Тщательное изучение конвейера будет проходить в следующих уроках, но пока что достаточно понимать, что прежде чем достичь растеризатора (т.е. отрисовка точки, линии и треугольников используя экранные координаты) видимые вершины имеют X, Y и Z координаты в отрезке [-1.0,1.0]. Растеризатор отображает координаты в пространство экрана (т.е, если ширина экрана равна 1024, то X координата -1.0 отобразится в 0, а 1.0 в 1023). Наконец, растеризатор отрисовывает примитивы в соответствии с топологией, которая указанна в вызове отрисовки (об этом ниже, в разборе исходников). Так как мы не назначили никаких шейдеров для конвейера наши вершины не претерпевают трансформацию. Это значит, что нам достаточно дать значения в указанном диапазоне, что бы сделать их видимыми. Фактически, задав X Y координаты по нулям, вершина отобразится на пересечении осей - другими словами в центре экрана.

Установка GLEW: Основной сайт GLEW'a http://glew.sourceforge.net/. Большинство дистрибутивов GNU/Linux предоставляют скомпилированные пакеты. На Ubuntu установить, запустив следующую команду: apt-get install libglew1.6 libglew1.6-dev

Прямиком к коду!

#include <GL/glew.h>

Здесь мы подключаем заголовок GLEW'a. Если вы подключаете другие заголовки OpenGL, вам следует подключить его раньше остальных, иначе GLEW откажется работать. Так же для линковки программ использующих GLEW необходимо добавить '-lGLEW' в Makefile.

#include "math_3d.h"

В этом уроке мы начнем использовать вспомогательные структуры такие как вектор. Мы постепенно будем увеличивать этот заголовок.

GLenum res = glewInit();
if (res != GLEW_OK)
{
    fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
    return 1;
}

Теперь мы инициализируем GLEW и проверяем на ошибки. GLUT необходимо инициализировать раньше.

Vector3f Vertices[1];
Vertices[0] = Vector3f(0.0f, 0.0f, 0.0f);

Мы создаем массив из одного экземпляра структуры Vector3f (этот тип объявлен в math_3d.h) и задаём XYZ по нулям. Так задается точка в середине экрана.

GLuint VBO;

Мы назначим GLuint в качестве глобальной переменной для хранения указателя на буфер вершин. Позднее вы узнаете, что почти все (если не все) OpenGL объекты доступны через переменную типа GLuint.

glGenBuffers(1, &VBO);

OpenGL определяет несколько glGen* функций для генерации объектов переменных типов. Чаще всего они принимают 2 параметра: первый определяет количество объектов, которые вы хотите создать, и второй ссылка на массив типа GLuints для хранения указателя, по которому будут храниться данные (убедитесь, что размер массива соответствует требуемому!). Последующие вызовы функции не будут генерировать объекты этого типа, прежде чем вы их удалите с помощью glDeleteBuffers. Обратите внимание, что на данный момент вы не указываете цели использования буферов, они просто создаются в "общем" типе. Для указания задачи используется следующая функция.

glBindBuffer(GL_ARRAY_BUFFER, VBO);

OpenGL имеет довольно уникальный способ использования указателей. Во многих API указатель просто передается в соответствующую функцию и действие применяется по отношению к указателю. В OpenGL мы привязываем указатель к названию цели и затем запускаем команду на цель. Эти команды ограничивают изменения значения по указателю, пока другой не будет ограничен взамен этого или вызов примет в качестве указателя 0. Параметр GL_ARRAY_BUFFER означает, что буфер будет хранить массив вершин. Можно указать другой параметр GL_ELEMENT_ARRAY_BUFFER он показывает, что индексы вершин хранятся в другом буфере. Остальные параметры будут объяснены в дальнейших уроках.

glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

После связывания нашего объекта, мы наполняем его данными. Вызов выше принимает название цели (такое же как и при привязывании), размер данных в байтах, адрес массива вершин, и флаг, который обозначает использование паттернов для этих данных. Так как мы не собираемся изменять значения буфера, указываем GL_STATIC_DRAW. Его противоположность GL_DYNAMIC_DRAW. Это только рекомендации для OpenGL, но действительно важно назначать как можно больше флагов. Драйвер может положиться на них для эвристики оптимизации (например, указание места в памяти для хранения буфера).

glEnableVertexAttribArray(0);

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

glBindBuffer(GL_ARRAY_BUFFER, VBO);

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

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

Этот вызов говорит конвейеру как воспринимать данные внутри буфера. Первый параметр указывает на индекс атрибута. В нашем случае мы знаем, что это 0 по-умолчанию, но когда мы начнем использовать шейдеры, нужно будет указать либо явно указать индексы, либо запросы на них. Второй параметр - это количество компонентов в атрибуте (3 для X, Y и Z). Третий параметр - тип данных для каждого компонента. Следующий - хотим ли мы нормализовать атрибуты перед использованием в конвейере. В нашем случае мы хотим, что бы данные передавались не именными. Пятый параметр (названный "шаг") является числом байтов между 2 экземплярами атрибута. Поскольку мы храним только один атрибут (например, буфер хранит только координаты вершины) и данные плотно упакованы, мы передаем нулевое значение. Если мы имеем массив структур, которые содержат координаты вершины и нормали (а это вектор из 3-х чисел типа float) мы передадим размер структуры в байтах (6 * 4 = 24). Последний параметр - полезен в случае с предыдущим примером. Нам нужно, указать смещение в структуре, которую получит наш конвейер. Тогда в случае структуры с координатами позиции и нормалями смещение по позиции равно 0, а смещение нормали 12.

glDrawArrays(GL_POINTS, 0, 1);

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

OpenGL предоставляет несколько различных типов вызовов отрисовки и каждый используется в разных случаях. Их можно разделить на 2 категории: порядковая и индексированная отрисовка. Порядковая проще. GPU обходит буфер вершин проходя через вершины одна за другой и интерпретирует их в соответствии с типом, указанном в вызове функции. Например, если указать GL_TRIANGLES, то вершины 0-2 станут первым треугольником, 3-5 вторым и т.д. Если вы хотите использовать одну вершину более чем в одном треугольнике, то необходимо дублировать её в буфере вершин, а это пустая трата памяти.

Индексированная отрисовка гораздо сложнее и для её использования приходится вводить дополнительные буферы. Буфер индексов хранит номера вершин в вершинном буфере. GPU сканирует буфер индексов и аналогично описанному выше вершины с индексами 0-2 становятся первым треугольником и т.д. Если необходимо использовать некоторые вершины в 2 треугольниках необходимо просто указать индекс дважды в буфере индексов. Буферу вершин необходимо только хранить одну копию. Индексированная отрисовка более подходящая для игр, поскольку большинство 3D моделей состоят из треугольников, представляющих некоторую поверхность (кожа лица, стена замка и т.д.) с большим количество общих вершин.

В этом уроке мы используем простую отрисовку - glDrawArrays. Она порядковая, поэтому мы не использовали буфер индексов. Мы указали тип как точку, это значит, что каждая вершина - одна точка. Следующий параметр это индекс первой вершины для отрисовки. В нашем случае мы хотим брать вершины с начала буфера, поэтому мы указали 0. Это позволяет хранить несколько моделей в одном буфере и затем указывать одну, задав её начальное положение. Последний параметр - количество вершин для отрисовки.

glDisableVertexAttribArray(0);

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

powered byDisqus