Урок 4: Цветной куб
Добро пожаловать на наш четвертый урок! Сегодня мы займемся:
- Рисованием куба, вместо скучного треугольника
- Добавлением цвета
- Изучением Буфера Глубины (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)!
На этом наш урок закончен. В следующем уроке мы поговорим о текстурах.