Урок 2. Вывод 3d геометрии на экран в OpenGL4

13.08.2012

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

Обзор архитектуры для вывода геометрии в OpenGL

В отличие от предыдущих версий, в OpenGL отображение чего-либо на экране означает не просто вызов некого метода интерфейса, который сделает и подготовит видеоадаптер для нас. Чтобы отобразить что-то в OpenGL4.x нужно самостоятельно подготовить все данные для отправки их в видеоадаптер, а потом уже вызывать данный метод. Итак, рассмотрим из чего состоит графический конвейер:

Данные, из которых состоит любой объект представляются в виде вершинного буфера
а так же индексного буфера. В обозначениях этого рисунка это VertexBuffer и IndexBuffer соответственно.

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

После того как данные вершин обработаны, они поступают из вершинного шейдера в пиксельный. В нем происходит обработка каждой точки полигона. То есть он закрашивается. При этом цвет закраски задается в пиксельном шейдере. Также пиксельный шейдер можно запрограммировать так, чтобы цвета брались из текстуры – так и происходит в 3d играх если модель имеет текстуру. Но предположим, что вершинный шейдер выдал для каждой из трех вершин свой цвет, каким должен получиться треугольник? Правильно, трехцветным. Пиксельный шейдер интерполирует цвета для всех входящих вершин, так что они будут плавно переходить от одной вершины к другой. Для этого не нужно добавлять в пиксельный шейдер специальный код, треугольник станет цветным только потому что выходящими данными вершинного шейдера является полигон – то есть набор из трех точек, а входящими данными для пиксельного шейдера уже не является полигон и координаты этих точек. Сам адаптер плавно передвигает «точку» рисования между вершинами постепенно заполняя треугольник(полигон) сплошным цветом и при этом для каждой маленькой суб-точки закраски вызывая пиксельный шейдер. Надо сказать что отсюда следует непреложный вывод: никогда не грузите пиксельный шейдер чрезмерно кодом – нескольких строк кода почти всегда достаточно, а при лишней загрузке пиксельного шейдера могут быть серьезные тормоза – точек на экране очень много.

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

Инициализация геометрии: InitGeometry

Теперь разберемся, что же нужно сделать чтобы вывести геометрию на экран в OpenGL. Перед тем как приступать к изучении примера, вы можете скачать его исходный код, расположенный тут. Напомним, что в предыдущем уроке мы создали функцию InitContext. В этой программе также есть эта функция а также остальные, которые мы рассматривали в предыдущем примере, но мы их заново рассматривать не будем а просто включим в нашу программу. Функция InitGeometry будет выполняться перед циклом обработки сообщений. Итак, подготовим и создадим геометрию в функции InitGeometry. Мы разобьем все наше рисование на две части – на инициализацию – сначала мы подготовим модель для рисования и на рендеринг – далее мы можем рисовать модель столько раз сколько нужно. В данной части урока мы рассмотрим инициализацию. Первое что мы сделаем в нашей функции InitGeometry — это загрузим код шейдера. На самом деле это просто, просто укажем имя файла шейдера в форматах .vertи .frag и вызовем фукнцию для загрузки:

// Загрузка вершинного и пиксельного шейдера
shVertex.loadShader("data\\shaders\\shader.vert", GL_VERTEX_SHADER); 
shFragment.loadShader("data\\shaders\\shader.frag", GL_FRAGMENT_SHADER); 
 
spMain.createProgram(); 
spMain.addShaderToProgram(&shVertex); 
spMain.addShaderToProgram(&shFragment); 
 
spMain.linkProgram(); 
spMain.useProgram(); //загружаемэффект

Инициализация геометрии – создание вершин: InitGeometry

Теперь зададимся вопросом: предположим у нас имеется задумка о том, что будет из себя представлять наш будущий объект, короче – мы имеем конкретные координаты 3dвершин, имеем их количество а также знаем номера вершин которые будет группироваться в треугольники. Но как все эти данные установить в 3dадаптер в качестве вершинного и индексного буфера?

Ответ несложный. Нужно сначала определить эти данные, потом создать для них обобщенный буфер, и затем перед рендером модели просто установить этот буфер в OpenGL(одновременно может быть установлен только один буфер – то есть устанавливаем буфер для одной модели, рисуем её, потом устанавливаем для другой, рисуем и т.д). Итак, создадим обобщенный буфер модели, содержащий вершинный и индексный буферы:

// Устанавливаем суббуфер координат вершин
fQuad[0] = -0.25f; fQuad[1] = -0.25f; fQuad[2] = 0.0f; 
fQuad[3] = 0.25f; fQuad[4] = -0.25f; fQuad[5] = 0.0f; 
fQuad[6] = 0.25f; fQuad[7] = 0.25f; fQuad[8] = 0.0f; 
fQuad[9] = -0.25f; fQuad[10] = 0.25f; fQuad[11] = 0.0f; 
 
// Устанавилваемсуббуфер цветов вершин
fQuadColor[0] = 1.0f; fQuadColor[1] = 0.0f; fQuadColor[2] = 0.0f; 
fQuadColor[3] = 0.0f; fQuadColor[4] = 1.0f; fQuadColor[8] = 0.0f; 
fQuadColor[6] = 0.0f; fQuadColor[7] = 0.0f; fQuadColor[5] = 1.0f; 
fQuadColor[9] = 1.0f; fQuadColor[10] = 1.0f; fQuadColor[11] = 0.0f;
 
//Устанавилваемсуббуфер индексов
int iIndices[]= {
      0, 1, 2,
      0, 2, 3,
};
 
glGenVertexArrays(1, uiVAO); // ГенерируемVAO 
glGenBuffers(3, uiVBO); //Генерируем три буфера VBO - координат, цветов и индексный
 
// Инициализируем и заполняем обобщенный буфер
glBindVertexArray(uiVAO[1]); 
 
glBindBuffer(GL_ARRAY_BUFFER, uiVBO[0]); 
glBufferData(GL_ARRAY_BUFFER, 12*sizeof(float), fQuad, GL_STATIC_DRAW); 
glEnableVertexAttribArray(0); 
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); 
 
glBindBuffer(GL_ARRAY_BUFFER, uiVBO[1]); 
glBufferData(GL_ARRAY_BUFFER, 12*sizeof(float), fQuadColor, GL_STATIC_DRAW); 
glEnableVertexAttribArray(1); 
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
 
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, uiVBO[2]); 
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof[iIndices], iIndices, GL_STATIC_DRAW);

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

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

Так что для координат отдельный, для нормалей отдельный, для цветов вершин тоже отдельный суббуфер, и еще сюда же входит и индексный суббуфер. Таким образом, если входящий формат вершинногошейдера насчитывает две переменные, — например, как в этом случае POSITION и COLOR — , вы должны создать три суббуфера – два для этих переменных и один индексный. Eсли формат вершинного шейдера три переменные, то нужно создать четыре суббуфера и т.п. Теперь на секунду заглянем в код функции Render, то есть посмотрим, как мы будем отображать наш обобщенный буфер:

glBindVertexArray(uiVAO[0]);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);

Таким образом, такой простой командой мы осуществим рендер модели. Первая функция устанавливает текущий обобщенный буфер по его идентификатору uiVAO[0]. Вторая функция осуществляет рендериндексированной примитивы. В качестве второго параметра этой функции мы должны указать количество индексов индексного буфера. Для нашего примера это 6. Не забывайте, что после того, как ваше приложение завершиться, и перед удалением OpenGL контекста вы должны произвести очистку буферов.

glDeleteBuffers(3, uiVBO); 
glDeleteVertexArrays(1, uiVAO);

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

Обзор приложения

Теперь остается только скомпилировать и запустить приложение.

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

Обзор шейдера для нашего приложения

Практически при любом отображении чего-либо на экран в DirectX10 необходимо использовать шейдеры. Шейдер представляет из себя программу на языке GLSL. Этот язык подобен языку C. В предыдущих разделах мы загрузили шейдер, но совершенно его не рассмотрели. Сейчас мы займемся этим. Шейдеры делятся на два типа – вершинный и пиксельный. Рассмотрим вершинный шейдер. Его назначение – просто преобразовывать координаты вершин. Если мы не будем их преобразовывать, то все координаты будут выведены на экран в таком пространстве:

То есть, если мы отобразим треугольник, находящейся в плоскости XY, то он предстанет в неизмененном виде. Однако если треугольник будет в плоскости XZ, то он будет в виде тонкой линии или не будет виден совсем. Если же мы установим камеру, то мы сможем вращать её вокруг треугольника, при этом также будут пересчитываться его координаты для вывода в вышеуказанное пространство экрана(-1,1),(-1,1). Пересчетом координат и занимается вершинный шейдер – для этого каждая координата умножается на несколько матриц преобразования.
Стоит отметить, что если мы не будем умножать координаты точек на матрицы, или будем умножать их на еденичные матрицы – что соответствует отсутствию всякого преобразования, то это также можно использовать, например, для отображения всевозможных надписей, элементов интерфейса и другой плоской графики поверх основной 3d графики сцены. Также такая техника подойдет для игр где графика выполнена в виде спрайтов. На самом деле она настолько востребована, что системы частиц (партиклы) в какой то мере также используеют только частичное преобразование 3d координат, в результате чего частицы всегда выводятся на экран плоско, однако облака этих частиц, конечно же находятся в некотором объеме.
Но мы отступили от темы. Рассмотрим код нашего шейдера. Обратимся к вершинному шейдеру:

#version 330
 
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inColor;
 
smooth out vec3 theColor;
 
void main()
{
	gl_Position = vec4(inPosition, 1.0);
	theColor = inColor;
}

Входящими данными нашего вершинного шейдера являются переменны inPosition и inColor. Это соответствует формату, который мы рассматривали ранее в уроке, каждая вершина содержит коориданату и цвет. Сама процедура вершинного шейдера называется main. Как вы можете видеть, шейдер имеет на выходе практически тот-же формат что и на входе. В него входят координаты и выходят тоже координаты, только изменененные. В следующих уроках входящие координаты будут умножаться на матрицы проекций и трансформаций, в текущем уроке мы мнимо умножаем координаты на еденичные матрицы, то есть, входящие данные inPosition равны выходящим данным gl_Position, что соответствует отсутствию всякого преобразования. Однако в следующих уроках мы установим матрицы для камеры и трансформаций и немного оживим сцену, поворачивая камеру на любые углы и рассматривая модель с различных сторон, а также приближая и отдаляя камеру.
Теперь рассмотрим пиксельный шейдер. Он совсем небольшой и входящими данными являются координаты и цвет. Но координаты нам теперь уже не нужны, так как видеоадаптер уже поставил точку в нужное место экрана, и спрашивает каким цветом её закрасить.

#version 330
 
smooth in vec3 theColor;
out vec4 outputColor;
 
void main()
{
	outputColor = vec4(theColor, 1.0);
}

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

Заключение

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

Заполнен: OpenGL4
Присвоен тэг:

avatar

Об Авторе ()

Планета назначения, JO - 8703 - IV, обозначается светлой точкой на объемном экране корабля. К тому времени, как Робоид нанялся на юпитерианский круговой рейс, ему следовало бы стать главным инженером, но после Суховодного пионерского его вышибли, внеся в черный список, и высадили в Луна-Сити за то, что вместо слежения за приборами он провел время, программируя синтезатор пищи корабля строжайше запрещенной корабельными инструкциями последовательностью синтеза пива.

Комментирование закрыто.

Наверх