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

  • Рисованием куба, вместо скучного треугольника
  • Добавлением цвета
  • Изучением Буфера Глубины (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, 12*3); // 12*3 индексов начинающихся с 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)!

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