Добро пожаловать на наш четвертый урок! Сегодня мы займемся:

  • Рисованием куба, вместо скучного треугольника
  • Добавлением цвета
  • Изучением Буфера Глубины (Z-Buffer)

Рисование куба

Куб имеет 6 прямоугольных граней, однако OpenGL знает только о треугольниках, поэтому все, что мы делаем - это выводим 12 треугольников (по 2 на каждую грань). Задаем вершины точно также, как мы делали это для треугольника: ```

// Наши вершины. Три вещественных числа дают нам вершину. Три вершины дают нам треугольник. // Куб имеет 6 граней или 12 треугольников, значит нам необходимо 12 * 3 = 36 вершин для описания куба. static const GLfloat g_vertex_buffer_data[] = { -1.0f,-1.0f,-1.0f, // Треугольник 1 : начало -1.0f,-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, // Треугольник 1 : конец 1.0f, 1.0f,-1.0f, // Треугольник 2 : начало -1.0f,-1.0f,-1.0f, -1.0f, 1.0f,-1.0f, // Треугольник 2 : конец 1.0f,-1.0f, 1.0f, -1.0f,-1.0f,-1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f,-1.0f,-1.0f, -1.0f,-1.0f,-1.0f, -1.0f,-1.0f,-1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f,-1.0f, 1.0f,-1.0f, 1.0f, -1.0f,-1.0f, 1.0f, -1.0f,-1.0f,-1.0f, -1.0f, 1.0f, 1.0f, -1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f, -1.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f,-1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f }; ```

OpenGL буфер создается, привязывается, заполняется и конфигурируется стандартными функциями (glGenBuffers, glBindBuffer, glBufferData, glVertexAttribPointer); Смотрите Урок 2, чтобы освежить память. Сама процедура вывода не меняется и все, что меняется - это количество вершин, которые мы будем выводить: ```

// Вывести треугольник glDrawArrays(GL_TRIANGLES, 0, 123); // 123 индексов начинающихся с 0. -> 12 треугольников -> 6 граней. ```

Несколько заметок по этому коду:

  • Сейчас наша модель статична, таким образом, чтобы изменить ее, нам понадобится изменить исходных код, перекомпилировать проект и надеяться на лучшее. Мы узнаем как загружать модели во время выполнения программы в Уроке 7.
  • Каждая вершина в нашем случае указывается как минимум три раза (например посмотрите на “-1.0f, -1.0f, -1.0f” в коде выше). Это бесполезная растрата памяти и вычислительной мощности. Мы узнаем, как избавиться от дублирующихся вершин в Уроке 9.

Добавление цвета

Цвет в понимании OpenGL - это тоже самое, что и позиция, т. е. просто данные. В терминологии они называются атрибутами. Мы уже работали с ними, с помощью таких функций, как: glEnableVertexAttribArray() и glVertexAttribPointer(). Теперь мы добавим еще один атрибут и код для этого действия будет очень похож.

Первым делом мы объявляем наши цвета - один RGB триплет на вершину. Этот массив был сгенерирован случайно, поэтому результат будет выглядеть не очень красиво, однако ничто не мешает вам сделать его лучше: ```

// Один цвет для каждой вершины static const GLfloat g_color_buffer_data[] = { 0.583f, 0.771f, 0.014f, 0.609f, 0.115f, 0.436f, 0.327f, 0.483f, 0.844f, 0.822f, 0.569f, 0.201f, 0.435f, 0.602f, 0.223f, 0.310f, 0.747f, 0.185f, 0.597f, 0.770f, 0.761f, 0.559f, 0.436f, 0.730f, 0.359f, 0.583f, 0.152f, 0.483f, 0.596f, 0.789f, 0.559f, 0.861f, 0.639f, 0.195f, 0.548f, 0.859f, 0.014f, 0.184f, 0.576f, 0.771f, 0.328f, 0.970f, 0.406f, 0.615f, 0.116f, 0.676f, 0.977f, 0.133f, 0.971f, 0.572f, 0.833f, 0.140f, 0.616f, 0.489f, 0.997f, 0.513f, 0.064f, 0.945f, 0.719f, 0.592f, 0.543f, 0.021f, 0.978f, 0.279f, 0.317f, 0.505f, 0.167f, 0.620f, 0.077f, 0.347f, 0.857f, 0.137f, 0.055f, 0.953f, 0.042f, 0.714f, 0.505f, 0.345f, 0.783f, 0.290f, 0.734f, 0.722f, 0.645f, 0.174f, 0.302f, 0.455f, 0.848f, 0.225f, 0.587f, 0.040f, 0.517f, 0.713f, 0.338f, 0.053f, 0.959f, 0.120f, 0.393f, 0.621f, 0.362f, 0.673f, 0.211f, 0.457f, 0.820f, 0.883f, 0.371f, 0.982f, 0.099f, 0.879f }; ```

Создание, привязывание и заполнения буфера такое же, как и для предыдущего буфера: ```

GLuint colorbuffer; glGenBuffers(1, &colorbuffer); glBindBuffer(GL_ARRAY_BUFFER, colorbuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_color_buffer_data), g_color_buffer_data, GL_STATIC_DRAW); ```

Конфигурация тоже идентична: ```

// Второй буфер атрибутов - цвета glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, colorbuffer); glVertexAttribPointer( 1, // Атрибут. Здесь необязательно указывать 1, но главное, чтобы это значение совпадало с layout в шейдере.. 3, // Размер GL_FLOAT, // Тип GL_FALSE, // Нормализован? 0, // Шаг (void*)0 // Смещение ); ```

Теперь, в вершинном шейдере мы имеем доступ к дополнительному буферу: ```

// Не забывайте, что значение “1” здесь должно быть идентично значению атрибута в glVertexAttribPointer layout(location = 1) in vec3 vertexColor; ```

В нашем случае мы не будем выполнять в вершинном шейдере какой-то дополнительной работы, поэтому просто передадим информацию в Фрагментный шейдер. ```

// Выходные данные. Будут интерполироваться для каждого фрагмента. out vec3 fragmentColor;

void main(){

[...]

// Цвет каждой вершины будет интерполирован для получения цвета
// каждого фрагмента
fragmentColor = vertexColor; } ```

В Фрагментом шейдере мы опять объявляем fragmentColor: ```

// Интерполированные значения из вершинного шейдера in vec3 fragmentColor; ```

… и копируем это в финальный выходной цвет: ```

// Выходные данные out vec3 color;

void main(){ // Выходной цвет = цвету, указанному в вершинном шейдере, // интерполированному между 3 близлежащими вершинами. color = fragmentColor; } ```

И вот, что мы получили в итоге:

Ух, выглядит как-то уродливо. Давайте посмотрим что происходит, когда мы выводим треугольник, который находится дальше, а потом треугольник, который находится ближе (far - дальний, near - ближний):

Это правильно, и теперь посмотрим как это будет в обратном порядке:

Выходит, что дальний треугольник перекрывает ближний, вместо того, чтобы быть позади. Это тоже самое, что произошло и с нашим кубом. Некоторые грани, которые должны быть невидимы были отрисованы последними и в итоге закрыли собой видимые. Здесь нам на помощь придет Буфер глубины (Z-Buffer).

Заметка 1: Если вы не видите проблемы, то попробуйте сменить позицию камеры в (4, 3, -3)

Заметка 2: Если “цвет, как и позиция является атрибутом”, то почему мы должны вводить переменную vec3 fragmentColor и работать с цветом через нее? Потому что позиция - это специальный атрибут и без него OpenGL будет просто не знать где отобразить треугольник, поэтому в вершинном шейдере есть встроенная переменная gl_Position.

Буфер глубины (Z-Buffer)

Решение проблемы заключается в хранении глубины (т. е. “Z” компоненты) каждого фрагмента в буфере и всякий раз, когда вы хотите вывести фрагмент, вам надо будет проверять, является ли он ближним или дальним.

Вы можете реализовать это сами, но гораздо более простым и элегантным решением будет использовать функционал OpenGL для этих целей: ```

// Включить тест глубины glEnable(GL_DEPTH_TEST); // Фрагмент будет выводиться только в том, случае, если он находится ближе к камере, чем предыдущий glDepthFunc(GL_LESS); ```

Вам также необходимо очищать буфер глубины перед каждым кадром: ```

// Очистка экрана glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ```

И этого достаточно, чтобы решить нашу проблему.

Упражнения

  • Нарисуйте куб и треугольник в разных позициях. Для решения вам понадобится 2 MVP-матрицы, чтобы выполнить 2 вызова процедуры вывода в главном цикле, однако шейдер вам нужен только 1.
  • Попробуйте изменить значения цветов. Например, вы можете заполнять массив цветовых атрибутов случайными значениями во время запуска программы. Можете сделать значение цвета зависимым от позиции вершины. Можете попробовать что-нибудь другое, что придет вам в голову :) Если вы не знаете как заполнить массив во время выполнения программы, то вот так это выглядит в Си:

static GLfloat g_color_buffer_data[12*3*3];
for (int v = 0; v < 12*3 ; v++){
    g_color_buffer_data[3*v+0] = здесь укажите значение красной компоненты цвета;
    g_color_buffer_data[3*v+1] = здесь зеленой;
    g_color_buffer_data[3*v+2] = и наконец значение синей компоненты;
}
  • После выполнения предыдущих упражнений попробуйте сделать так, чтобы цвета менялись каждый кадр. Здесь вам понадобится вызывать glBufferData в каждом кадре. Убедитесь, что перед этим не забыли привязать соответствующий буфер (glBindBuffer)!

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