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

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

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

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

 1 // Наши вершины. Три вещественных числа дают нам вершину. Три вершины дают нам треугольник.
 2 // Куб имеет 6 граней или 12 треугольников, значит нам необходимо 12 * 3 = 36 вершин для описания куба.
 3 static const GLfloat g_vertex_buffer_data[] = {
 4     -1.0f,-1.0f,-1.0f, // Треугольник 1 : начало
 5     -1.0f,-1.0f, 1.0f,
 6     -1.0f, 1.0f, 1.0f, // Треугольник 1 : конец
 7     1.0f, 1.0f,-1.0f, // Треугольник 2 : начало
 8     -1.0f,-1.0f,-1.0f,
 9     -1.0f, 1.0f,-1.0f, // Треугольник 2 : конец
10     1.0f,-1.0f, 1.0f,
11     -1.0f,-1.0f,-1.0f,
12     1.0f,-1.0f,-1.0f,
13     1.0f, 1.0f,-1.0f,
14     1.0f,-1.0f,-1.0f,
15     -1.0f,-1.0f,-1.0f,
16     -1.0f,-1.0f,-1.0f,
17     -1.0f, 1.0f, 1.0f,
18     -1.0f, 1.0f,-1.0f,
19     1.0f,-1.0f, 1.0f,
20     -1.0f,-1.0f, 1.0f,
21     -1.0f,-1.0f,-1.0f,
22     -1.0f, 1.0f, 1.0f,
23     -1.0f,-1.0f, 1.0f,
24     1.0f,-1.0f, 1.0f,
25     1.0f, 1.0f, 1.0f,
26     1.0f,-1.0f,-1.0f,
27     1.0f, 1.0f,-1.0f,
28     1.0f,-1.0f,-1.0f,
29     1.0f, 1.0f, 1.0f,
30     1.0f,-1.0f, 1.0f,
31     1.0f, 1.0f, 1.0f,
32     1.0f, 1.0f,-1.0f,
33     -1.0f, 1.0f,-1.0f,
34     1.0f, 1.0f, 1.0f,
35     -1.0f, 1.0f,-1.0f,
36     -1.0f, 1.0f, 1.0f,
37     1.0f, 1.0f, 1.0f,
38     -1.0f, 1.0f, 1.0f,
39     1.0f,-1.0f, 1.0f
40 };

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

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

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

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

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

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

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

 1 // Один цвет для каждой вершины
 2 static const GLfloat g_color_buffer_data[] = {
 3     0.583f,  0.771f,  0.014f,
 4     0.609f,  0.115f,  0.436f,
 5     0.327f,  0.483f,  0.844f,
 6     0.822f,  0.569f,  0.201f,
 7     0.435f,  0.602f,  0.223f,
 8     0.310f,  0.747f,  0.185f,
 9     0.597f,  0.770f,  0.761f,
10     0.559f,  0.436f,  0.730f,
11     0.359f,  0.583f,  0.152f,
12     0.483f,  0.596f,  0.789f,
13     0.559f,  0.861f,  0.639f,
14     0.195f,  0.548f,  0.859f,
15     0.014f,  0.184f,  0.576f,
16     0.771f,  0.328f,  0.970f,
17     0.406f,  0.615f,  0.116f,
18     0.676f,  0.977f,  0.133f,
19     0.971f,  0.572f,  0.833f,
20     0.140f,  0.616f,  0.489f,
21     0.997f,  0.513f,  0.064f,
22     0.945f,  0.719f,  0.592f,
23     0.543f,  0.021f,  0.978f,
24     0.279f,  0.317f,  0.505f,
25     0.167f,  0.620f,  0.077f,
26     0.347f,  0.857f,  0.137f,
27     0.055f,  0.953f,  0.042f,
28     0.714f,  0.505f,  0.345f,
29     0.783f,  0.290f,  0.734f,
30     0.722f,  0.645f,  0.174f,
31     0.302f,  0.455f,  0.848f,
32     0.225f,  0.587f,  0.040f,
33     0.517f,  0.713f,  0.338f,
34     0.053f,  0.959f,  0.120f,
35     0.393f,  0.621f,  0.362f,
36     0.673f,  0.211f,  0.457f,
37     0.820f,  0.883f,  0.371f,
38     0.982f,  0.099f,  0.879f
39 };

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

1 GLuint colorbuffer;
2 glGenBuffers(1, &colorbuffer);
3 glBindBuffer(GL_ARRAY_BUFFER, colorbuffer);
4 glBufferData(GL_ARRAY_BUFFER, sizeof(g_color_buffer_data), g_color_buffer_data, GL_STATIC_DRAW);

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

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

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

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

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

 1 // Выходные данные. Будут интерполироваться для каждого фрагмента.
 2 out vec3 fragmentColor;
 3 
 4 void main(){
 5 
 6     [...]
 7 
 8     // Цвет каждой вершины будет интерполирован для получения цвета
 9     // каждого фрагмента
10     fragmentColor = vertexColor;
11 }

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

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

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

1 // Выходные данные
2 out vec3 color;
3 
4 void main(){
5     // Выходной цвет = цвету, указанному в вершинном шейдере,
6     // интерполированному между 3 близлежащими вершинами.
7     color = fragmentColor;
8 }

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

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

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

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

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

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

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

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

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

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

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

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

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

Упражнения

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

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