[Перевод] OpenGL-Tutorial. Урок 2. Первый треугольник
В прошлом уроке Вы научились создавать окно и собирать примеры. В этом уроке Вы научитесь рисовать объекты! Что же, прошу под кат.
Содержание
Базовые уроки:
- Урок 1. Создание окна
- Урок 2. Первый треугольник
- Урок 3. Матрицы
- Урок 4. Цветной куб
- Урок 5. Текстурированный куб
- Урок 6. Клавиатура и мышь
- Урок 7. Загрузка моделей
- Урок 8. Базовый шейдинг
Продвинутые уроки:
- Урок 9. VBO индексация
- Урок 10. Прозрачность
- Урок 11. 2D текст
- Урок 12. OpenGL расширения
- Урок 13. Normal Mapping
- Урок 14. Отрисовка на текстуру
- Урок 15. Lightmaps
- Урок 16. Shadow mapping
- Урок 17. Вращение
- Урок 18.1. «Билборды»
- Урок 18.2. Частицы
Всякое:
- Урок 9. VBO индексация
- Урок 10. Прозрачность
- Урок 11. 2D текст
- Урок 12. OpenGL расширения
- Урок 13. Normal Mapping
- Урок 14. Отрисовка на текстуру
- Урок 15. Lightmaps
- Урок 16. Shadow mapping
- Урок 17. Вращение
- Урок 18.1. «Билборды»
- Урок 18.2. Частицы
Предисловие
Это будет еще один длинный урок.
OpenGL 3 позволяет с легкостью делать довольно сложные вещи, но в противовес этому заставляет совершать слишком много действий для отрисовки простого треугольника.
Не забывайте тестировать код из статьи на регулярной основе.
Если ваша программу падает сразу после запуска, возможно вы запускаете ее из неправильной директории. Прочтите еще раз раздел про настройке Visual Studio из первого урока.
VAO
Не будем сейчас вдаваться в детали. Вам сейчас надо создать Vertex Array Object и установить его как активный:
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);
Выполните это один раз, после создания окна (создания контекста) и перед другими вызовами функций OpenGL.
Если хотите узнать побольше об VAO — то добро пожаловать в английскую википедию под
Экранные коодинаты
Треугольник характеризуется тремя точками. Когда в трехмерной графике идет разговор о «точках» имеются ввиду вершины (вертексы, vertex).
Вершина имеет 3 координаты: X, Y и Z. Вы можете представлять эти 3 координаты так:
- X — направо.
- Y — вверх.
- Z — на себя (да, имеено на себя, а не от себя).
Так же еще есть другой метод, называемый Правилом Правой Руки. Если вы сожмете руку в кулак, направите оттопыренный большой палец направо, а указательный наверх.
- X — это большой палец.
- Y — это указательный палец.
- Z — это средний палец.
Представлять координату Z в таком ключе довольно странно. Почему же это так? Короткий ответ: потому что сотни лет существования Математики Правой Руки, дадут Вам кучу полезных инструментов. И единственным минусом будет неинтуитивный Z.
Также стоит заметить, что вы можете двигать свою руку и координаты тоже будут двигаться. Поподробнее позже.
Так что нам понадобятся три 3D точки для того, что бы описать треугольник:
// Массив из 3 векторов, которые описывают 3 вершины
static const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
};
Первая вершина (-1, -1, 0). Это означает, что до тех пор, пока мы ее не трансформировали, она будет отображаться в координатах (-1, -1) на экране. Что же это значит? Ось экрана находится по центру, X — направо, как всегда, Y — вверх. Пример:
Вы не можете изменить это правило. Оно записано в вашей видеокарте. Так что (-1, -1) это нижний левый угол экрана. (1, -1) это нижний правый и (0, 1) это центр сверху. Так что наш треугольник займет большую часть экрана.
Отрисовка треугольника
Следующим шагом будет передача треугольника в OpenGL. Мы сделаем это с помощью создания буффера:
// Эта переменная будет описывать наш вершинный буффер
GLuint vertexbuffer;
// Генерируем 1 буффер и размещаем его идентификатор в vertexBuffer
glGenBuffers(1, &vertexbuffer);
// Устанавливаем сгенерированный буффер, как активный
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
// Передаем наши вершины в OpenGL
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
Это надо сделать только 1 раз. Теперь в нашем основном цикле, где раньше мы ничего не отрисовывали, мы можем отрисовать треугольник:
// Первый буффер-аттрибут : вершины
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
0, // Аттрибут №0. Нет особой причины указывать именно 0, но этот номер должен совпадать с номером из шейдера.
3, // Количество
GL_FLOAT, // Тип
GL_FALSE, // Нормализован?
0, // Шаг
(void*)0 // Смещение
);
// Отрисовываем треугольник!
glDrawArrays(GL_TRIANGLES, 0, 3); // Начинаем с 0 вершины; всего 3 вершины -> 1 треугольник
glDisableVertexAttribArray(0);
Если вам повезет — то при запуске вы получите белый треугольник.
Но если у вас все также черный экран — то значит у вас отрисовывается черный треугольник на черном фоне. Что бы это исправить можно вызвать glClearColor и glClear перед каждой отрисовкой. Что изменит цвет фона. Либо задать цвет треугольнику. Чем мы сейчас и займемся.
Шейдеры
Компиляция шейдеров
В самой простой конфигурации нам понадобится 2 шейдера: один называется «Вершинным шейдером», а другой «Фрагментным шейдером». Вершинный шейдер вызывается для каждой вершины, в то время, когда Фрагментный шейдер вызывается для каждого сэмпла. У нас используется 4х кратный antialising, а значит у нас по 4 сэмпла на пиксель.
Шейдеры программируются на языке GLSL: Graphics Library Shader Language, который является частью OpenGL. В отличии от C или Java, GLSL компилируется во время исполнения программы, что означает, что вы должны компилировать шейдеры при каждом запуске программы.
Обычно для каждого шейдера отводится отдельный файл. К примеру у нас есть SimpleFragmentShader.fragmentshader и SimpleVertexShader.vertexshader. Расширение может быть любым. Хоть .txt или .glsl.
Вот код. Не обязательно полностью понимать код, поскольку он вызывается лишь один раз в программе, так что комментариев должно быть достаточно. Так как эта функция будет использоваться во всем уроках, она будет помещена в отдельный файл common/loadShader.cpp. Заметьте, что также как и к буфферам, к шейдерам доступ осуществляется по их индексу. Реализация спрятана в драйвере.
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){
// Создаем шейдеры
GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
// Считываем код вершинного шейдера из файла
std::string VertexShaderCode;
std::ifstream VertexShaderStream(vertex_file_path, std::ios::in);
if(VertexShaderStream.is_open()){
std::string Line = "";
while(getline(VertexShaderStream, Line))
VertexShaderCode += "\n" + Line;
VertexShaderStream.close();
}else{
printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !\n", vertex_file_path);
getchar();
return 0;
}
// Считываем код фрагментного шейдера из файла
std::string FragmentShaderCode;
std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in);
if(FragmentShaderStream.is_open()){
std::string Line = "";
while(getline(FragmentShaderStream, Line))
FragmentShaderCode += "\n" + Line;
FragmentShaderStream.close();
}
GLint Result = GL_FALSE;
int InfoLogLength;
// Компилируем вершинный шейдер
printf("Compiling shader : %s\n", vertex_file_path);
char const * VertexSourcePointer = VertexShaderCode.c_str();
glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
glCompileShader(VertexShaderID);
// Проверяем вершинный шейдер
glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
if ( InfoLogLength > 0 ){
std::vector VertexShaderErrorMessage(InfoLogLength+1);
glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
printf("%s\n", &VertexShaderErrorMessage[0]);
}
// Компилируем фрагментный шейдер
printf("Compiling shader : %s\n", fragment_file_path);
char const * FragmentSourcePointer = FragmentShaderCode.c_str();
glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
glCompileShader(FragmentShaderID);
// Проверяем фрагментный шейдер
glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
if ( InfoLogLength > 0 ){
std::vector FragmentShaderErrorMessage(InfoLogLength+1);
glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
printf("%s\n", &FragmentShaderErrorMessage[0]);
}
// Соединяем шейдеры в программу
printf("Linking program\n");
GLuint ProgramID = glCreateProgram();
glAttachShader(ProgramID, VertexShaderID);
glAttachShader(ProgramID, FragmentShaderID);
glLinkProgram(ProgramID);
// Проверяем программу
glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
if ( InfoLogLength > 0 ){
std::vector ProgramErrorMessage(InfoLogLength+1);
glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
printf("%s\n", &ProgramErrorMessage[0]);
}
glDetachShader(ProgramID, VertexShaderID);
glDetachShader(ProgramID, FragmentShaderID);
glDeleteShader(VertexShaderID);
glDeleteShader(FragmentShaderID);
return ProgramID;
}
Наш вершинный шейдер
Давайте начнем с вершинного шейдера. Первая строка скажет компилятору, что мы используем синтаксис OpenGL 3.
#version 330 core
Вторая строка описывает входные данные:
layout(location = 0) in vec3 vertexPosition_modelspace;
Давайте опишем эту строку по подробнее:
- «vec3» это трехкомпонентный вектор в GLSL. Он похож (но отличен) на glm: vec3, который мы использовали для описания треугольника. Важно помнить, что если мы используем 3 компонентный вектор — то мы должны использовать 3 компонентный вектор в GLSL.
- «layout (location = 0)» ссылается на буффер, который будет использоваться для заполнения vertexPosition_modelspace. Каждая вершина имеет множество аттрибутов: позиция, один или несколько цветов, один или несколько текстурных координат (UV) и т.д. OpenGL не знает, что определенный атрибут — это цвет. Он просто видит трехкомпонентный вектор. Поэтому мы должны сообщить какой буффер за что отвечает. Сообщаем мы при помощи ключевого слова layout и указании индекса, который должен быть таким же, как и первый аргумент glVertexAttribPointer. Значение 0 не важно, оно может быть любым. (Но не больше, чем glGetIntegerv (GL_MAX_VERTEX_ATTRIBX, &v)).
- «vertexPosition_modelSpace» — название аргумента.
- «in» — означает, что это входной аргумент. Вскоре мы познакомимся в «out».
Функция, вызываемая для каждой вершины, называется main, прямо как в C:
void main(){
Наша main функция будет просто устанавливать позицию вершины на координаты, указанные в буфере. Так что если мы передает (1, 1) одна из вершин треугольника будет в верхнем правом углу экрана. В следующих уроках мы познакомимся с более интересными вычислениями, которые можно производить над входными данными.
gl_Position.xyz = vertexPosition_modelspace;
gl_Position.w = 1.0;
}
«gl_Position» одна из нескольких встроенных переменных. Вы должны передать ей какое-то значение. Все остальное — не обязательно. (Про «все остальное» мы поговорим в 4 уроке)
Наш фрагментный шейдер
Для нашего первого фрагментного шейдера мы реализуем нечто очень простое: раскраску каждого фрагмента в красный цвет. (Помните, что на каждый пиксель 4 фрагмента, поскольку мы используем 4х AA)
#version 330 core
out vec3 color;
void main(){
color = vec3(1,0,0);
}
Да, vec3(1, 0, 0) означает красный. Дело в том, что компьютерный экраны представляют цвет, как комбинацию из Красного, Зеленого и Синего (RGB). Так что (1, 0, 0) означает самый яркий красный, нет зеленого и нет синего.
Соединение всего этого
Перед главным циклом вызываем функцию LoadShaders.
// Создаем и компилируем нашу GLSL программу из шейдеров
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
Теперь внутри главного цикла в начале очищаем экран. Функция glClearColor (0.0f, 0.0f, 0.4f, 0.0f) установит цвета фона на синий. Для очистки экрана вызывается:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
А затем говорим OpenGL, какой шейдер мы используем:
// Используем наш шейдер
glUseProgram(programID);
// Отрисовка треугольника...
Вот и все. Вот наш красный треугольник на синем фоне.
В следующем уроке мы поговорим о трансформациях: как настроить камеру, двигать объекты и т.д.
Комментарии (9)
29 июля 2016 в 10:44
0↑
↓
Ось экрана находится по центру, X — направо, как всегда, Y — налево.
Может быть всё-таки «Y — вверх»?29 июля 2016 в 10:49
0↑
↓
Конечно! Опечатался. Сейчас поправим.
29 июля 2016 в 10:49
0↑
↓
Ссылка на первый урок «Урок 1. Создание окна» битая — на редактирование поста ссылается (29 июля 2016 в 10:49
0↑
↓
Да, и вправду. Исправил, спасибо.
29 июля 2016 в 10:57
+1↑
↓
Для работы с файлами используются iostreams, для вывода на консоль — printf. Каша из головы автора плавно перетекает в головы учеников.29 июля 2016 в 11:06 (комментарий был изменён)
0↑
↓
Да, соглашусь, что лучше было бы использовать постоянно что-то одно.
Как думаете имеет смысл рефакторить вывод в консоль в iostream? Там дальше тоже будет работа с файлами, она вообще осуществляется через fopen. С другой стороны — это урок по OpenGL, а не по C++.29 июля 2016 в 11:13
0↑
↓
Чем больше материал похож на, как любит выражаться Кармак, «solid rock», тем больше к нему доверия.С другой стороны, тяжбы на тему «потоки в C++ медленные!11111» длятся достаточно давно с попеременным успехом. Для загрузки файлов я бы и вовсе рекомендовал использовать технологию отображения в память везде, где это только возможно.
29 июля 2016 в 11:19
0↑
↓
Вы, как я понимаю, имеете ввиду именно файлы, вроде шейдеров, которые и нужны, в общем то во полном их виде в опере. Потому что при загрузке моделей я не вижу особого смысла выгружать сразу весь файл в память. (Хотя это от формата зависит, конечно же).29 июля 2016 в 11:24
0↑
↓
Разумеется. Отображение файлов прекрасно подходит для несжатых текстур, например.