Урок 2. Первый треугольник

Наш очередной длинный урок.

OpenGL 3 позволяет делать сложные вещи достаточно просто, но в то же время такое простое действие, как вывод треугольника - сделать с нуля, не так то и легко.
Не забывайте копипастить код в ваш проект и пробовать!!!

VAO


Не буду вдаваться в подробности, но нам необходимо создать Vertex Array Object и установить его как текущий:
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);

Сделайте это сразу же после создания OpenGL окна(после инициализации OpenGL контекста) и перед любым из вызовов OpenGL.
Если вам сильно хочется узнать побольше про VAO, поищите описание в интернете, для текущего урока это не сильно важно.

Экранные Координаты


Треугольник можно задать тремя точками. Когда мы говорим про точки в 3д пространстве, мы обычно употребляем термин «вершина». Вершина содержит 3 координаты: X,Y и Z. Вы можете представить для себя эти координаты так:
  • X - вправо
  • Y - вверх
  •  Z - за спину
Но есть еще более наглядный способ представить себе эту систему координат: правило правой руки.
  • X – большой палец правой руки
  • Y – указательный палец правой руки
  • Z – средний палец правой руки.
Теперь если вы направите свой большой палец направо, указательный вверх, то средний будет показывать на вас.

Почему Z такое странное? Очень просто: просто за стони лет существования математики и правила правой руки, было создано много методов облегчающих расчеты которые ориентируются на этот неинтуитивный Z.
Кстати, заметьте, вы можете свободно двигать рукой, и ваши X, Y и Z тоже будут двигаться за вами. Но об этом позже.
И так, нам нужно три точки в 3д пространстве, чтобы нарисовать треугольник.
// Массив из 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). Что значит -1,-1? Видеокарта трактует экранные координаты так, что центр экрана это 0, а крайние точки: -1 и 1 по X и Y. Вот что у нас получается на широкоформатном мониторе:

Неудобно, да, но поменять это нельзя. Поэтому наш треугольник будет расположен по таким координатам: (-1,-1) это нижний левый угол. (1,-1) – нижний правый. (0,1) – верхняя центральная точка.

Рисуем наш треугольник

Нашим следующим шагом будет скормить эти вершины в OpenGL. Для этого необходимо сначала создать буфер:
// Идентификатор вершинного буфера
GLuint vertexbuffer;

// Сначала генерируем OpenGL буфер и сохраняем указатель на него в 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);

Эти операции нужно выполнить лишь один раз.
А теперь в главном цикле программы, где мы в прошлом уроке не рисовали ничего, теперь мы нарисуем наш прекрасный треугольничек.
// Первый буфер атрибутов: вершины
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
   0,                  // Атрибут 0. Сакрального смысла в нуле нет, но число должно совпадать с числом в шейдере
   3,                  // количество
   GL_FLOAT,           // тип
   GL_FALSE,           // нормализировано ли?
   0,                  // шаг
   (void*)0            // смещение в буфере
);

// Рисуем треугольник !
glDrawArrays(GL_TRIANGLES, 0, 3); //Начиная с вершины 0 и рисуем 3 штуки. Всего => 1 треугольник
glDisableVertexAttribArray(0);

А вот и результат:


Однако этот треугольник очень скучный, так как не цветной. Можно попробовать его разукрасить в веселенький красный цвет. Раскраску можно сделать с помощью шейдеров.

Шейдеры

В самом простом варианте нам понадобится два шейдера:
  • Вершинный Шейдер. Это шейдер который будет вызываться для каждой вершины.
  • Фрагментный Шейдер. Это шейдер который вызывается при растеризации на каждый пиксель. Если у нас 4х сглаживание, то фрагментный шейдер вызовется 4 раза на каждый пиксель.

Шейдеры в OpenGL программируются на языке GLSL: GL Shader Language, который есть частью OpenGL. В отличии от С или Java,GLSL компилируется программой при исполнении, что значит, что при каждом старте приложения ваши шейдеры будут перекомпилироваться.
Обычно разные шейдеры хранят в разных файлах. Поэтому и у нас будет два файла SimpleFragmentShader.fragmentshader и SimpleVertexShader.vertexshader. Расширение файла не важно, вы можете писать хоть .txt хоть .glsl.
А вот и код. Вам не нужно полностью его понимать, так как скорее всего он будет написан лишь раз в вашей программе, поэтому думаю, что моих комментариев будет достаточно. Этот код одинаковый для всех уроков, поэтому я вынес его в один файл: common/loadShader.cpp. Заметьте, шейдеры как и буферы не доступны прямо из программы: у нас есть лишь ID. Содержимое спрятано внутри драйвера и в памяти видеокарты.

    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();
    }

    // читаем фрагментный шейдер из файла
    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);
    std::vector<char> VertexShaderErrorMessage(InfoLogLength);
    glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
    fprintf(stdout, "%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);
    std::vector<char> FragmentShaderErrorMessage(InfoLogLength);
    glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
    fprintf(stdout, "%s\n", &FragmentShaderErrorMessage[0]);

    fprintf(stdout, "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);
    std::vector<char> ProgramErrorMessage( max(InfoLogLength, int(1)) );
    glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
    fprintf(stdout, "%s\n", &ProgramErrorMessage[0]);

    glDeleteShader(VertexShaderID);
    glDeleteShader(FragmentShaderID);
    return ProgramID;

Вершинный Шейдер

Давайте сначала напишем наш вершинный шейдер.
Первая строчка будет указанием компилятору, что мы хотим использовать синтаксис OpenGL 3:
#version 330 core
Вторая строчка объявляет входные данные:
layout(location = 0) in vec3 vertexPosition_modelspace;
Тут нужно объяснение поподробнее:
  • “vec3” это вектор  состоящий из трех компонентов в GLSL. Это почти то же самое(но не то), что и glm::vec3 что мы использовали для задания треугольника. Нужно не забывать, что если мы используем трехкомпонентный вектор в С++, то обязательно нужно использовать трехкомпонентный вектор и в GLSL.
  • layout(location=0)”указание для компилятора, в каком из буферов будет находится атрибут vertexPosition_modelspace. Каждая вершина может иметь множество атрибутов: Позиция, цвета, текстурные координаты, и еще кучу других вещей. GLSL не знает ничего про цвет, он видит лишь vec3. А наша задача – рассказать, что из себя представляет каждый входящий буфер. Это мы делаем устанавливая layout в то же самое значение, что и glVertexAttribPointer. Мы там поставили «0», но с таким же успехом могли поставить и 12(но это число не может быть больше чем glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v)). Самое главное, чтобы эти числа были одинаковы по обе стороны.
  • «vertexPosition_modelspace» - идентификатор «переменной». Можете назвать как угодно. В нашем случае будет содержать положение вершины которая обрабатывается в данный момент вершинным шейдером.
  • «in» значит, что это входящий параметр. Вскоре у нас будет и «out».

Точно так же как и в С в GLSL есть функция main:
void main(){
Тут все проще простого. Мы просто устанавливаем положение вершины в значение которое пришло к нам из буфера. Так что если нам пришло (1,1) одна из вершин треугольника будет в верхнем правом углу экрана. В последующих уроках я покажу, что в этом шейдере можно делать штуки и поинтереснее.

gl_Position.xyz = vertexPosition_modelspace;
    gl_Position.w = 1.0;
 }

gl_Position – это одна из встроенных переменных и мы просто обязаны установить какое-то значение в неё. Все остальные – по вашему желанию. Мы рассмотрим некоторые из «все остальные» в уроке 4.

Фрагментный Шейдер

В нашем первом фрагментном шейдере мы выполним очень простую операцию: установим цвет каждого фрагмента в красный цвет(не забудьте, что так как у нас 4х сглаживание, то на каждый пиксель у нас будет 4 фрагмента).

#version 330 core
out vec3 color;

void main(){
    color = vec3(1,0,0);
}

Да, vec3(1,0,0) – это красный цвет. Так происходит потому, что компьютер видит цвет, как совокупность Красного, Зеленого и Голубого. Поэтому (1,0,0) это Полностью Красный, нет ни зеленого ни голубого.

Объединяем все вместе

Перед главным циклом вызываем функцию LoadShaders:

// Загружаем и компилируем GLSL программу
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );

А теперь внутри главного цикла сначала очищаем экран, закрашивая его в темносиний(0,0f,0.0f,0.4f,0.0f).

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

А теперь говорим OpenGL, что нам нужно воспользоваться услугами шейдера:
// Подключаем шейдер
glUseProgram(programID);
// Рисуем треугольник...

Вуаля! Наш красненький треугольник!

В следующем уроке мы будем изучать трансформации: как установить камеру и двигать объекты...

7 комментариев:

  1. Видимо, в примере без шейдеров ошибка: белый треугольник не нарисуется, так как нигде не выставлялся белый цвет. У меня на чёрном фоне рисовался чёрный треугольник. Который, соответственно, не было видно, пока я не сделал:

    glClearColor(0.1, 0.1, 0.1, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);

    ОтветитьУдалить
    Ответы
    1. так там и не надо выставлять белый цвет, если нет фрагментного шейдера, который отвечает за цвет пикселей, примитив будет либо черный либо белый

      Удалить
  2. Собственно дошел до второго урока (правда я пытаюсь использовать glfw 3 версии там у некоторых функций поменяли название и некоторые параметры но 1 урок получилось переделать под 3 версию)
    а вот со 2 ым пока проблема. Я так понимаю еще какая та зависимость пока добавлял к стандартным (glfw3.lib;glfw3dll.lib;opengl32.lib;glu32.lib;glew32.lib) эти либы но похоже нужно еще что то.
    1>------ Построение начато: проект: windowOpenGLFW, Конфигурация: Debug Win32 ------
    1>LINK : warning LNK4098: библиотека по умолчанию "MSVCRT" противоречит использованию других библиотек; используйте параметр /NODEFAULTLIB:library
    1>mail.obj : error LNK2019: ссылка на неразрешенный внешний символ "unsigned int __cdecl LoadShaders(char const *,char const *)" (?LoadShaders@@YAIPBD0@Z) в функции _main
    1>C:\Users\Admin\Dropbox\OpenGL\glfwWindow\windowOpenGLFW\Debug\windowOpenGLFW.exe : fatal error LNK1120: неразрешенных внешних элементов: 1

    ОтветитьУдалить
  3. сначала добавил shader.cpp и shader.hpp в c:\Program Files\Microsoft Visual Studio 11.0\VC\include\common\
    была ошибка описанная ранее ^.
    после чего добавил эти два файла рядом с main.cpp и в визуал студии добавил к проекту папку, а к папке 2 этих элемента после чего проект запустился но вылетело исключение на этой строчек
    glGenVertexArrays(1, &VertexArrayID);

    сообщение
    Необработанное исключение по адресу 0x00000000 в windowOpenGLFW.exe: 0xC0000005: нарушение прав доступа при исполнении по адресу 0x00000000.

    ОтветитьУдалить
  4. Судя по ошибке, что-то где-то записывается по нулевому неинициалицированному указателю.
    Вряд ли ошибка конкретно в "glGenVertexArrays(1, &VertexArrayID);", скорее всего ты пропустил что-то где-то до этого. Не работал с glfw3, может быть там как-то нужно инициализировать его по другому? Почитай в доке.

    ОтветитьУдалить
  5. https://yadi.sk/i/e9lqxDh6oJfps
    У меня выдавалась такая ошибка. После каких-о манипуляций я вроде бы избавился от парочки ошибок. Уже и не помню каких.. Суть такова: я порыскал в интернете и нашёл следующий код:
    Fragment:
    #version 330 core
    out vec4 color;
    void main(){
    color = vec4(1,0,0,1);
    }
    Vertex:
    #version 330 core
    layout(location = 0) in vec3 Position;
    void main(){
    gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
    }
    и О ЧУДО! Треугольник перекрасился.

    ОтветитьУдалить
  6. как правильно указать путь к шейдеру и куда сам шейдер ложить?

    ОтветитьУдалить