OpenGL шейдеры: Вторая часть. Шейдер в контексте OpenGL с GLSL без ФГК

Блоговая публикация пользователя: Flashhell Эта публикация была перенесена из личного блога пользователя в общие разделы уровок сайта.
Необходимые знания:
Вам могут понадобится следующие статьи:

OpenGL шейдеры. Шейдер в контексте OpenGL с GLSL без ФГК

В прошлом уроке вы научились создавать самый примитивный шейдер и отрисовали самую простую атомарную единицу - треугольник, причем одно цвета. Данный урок будет посвящен шейдерам в контексте OpenGL с GLSL без ФГК.

В данном уроке мы рассмотрим следующее:
  • Построения матрицы проекции.
  • Использование множества VBO.
  • Передача данных из вершинного шейдера во фрагментный.
  • Отрисовка более сложных объектов.

Кроме того, вы познакомитесь с библиотекой GML (OpenGL Mathematics), которая облегчит работу с матрицами и векторами. Это библиотека написана по стандарту GLSL, то есть можно использовать векторы и матрицы также, как в шейдерах. Также мы напишем примитивный враппер для загрузки шейдеров в OpenGL.

Нам необходимо нарисовать разноцветный куб в перспективной проекции с помощью шейдеров, используя максимально совместимые с core profile OpenGL 3.2 и OpenGL ES 2.0 методы.

Для этого понадобится код и знания из предыдущего урока. Мы внесем некоторые изменения непосредственно в него. Но так как изменений было много, почти весь код будет представлен в этом уроке полностью.

Прежде чем перейти к изменениям, напишем небольшой враппер для удобно взаимодействия с шейдерами:

GlShader.h:

/*http://esate.ru, Flashhell*/


#ifndef GLSHADER_H
#define GLSHADER_H

#include 
#include "GL/glew.h"
#include "glm/glm.hpp"

using std::string;
using namespace glm;

class GlShader
{
        public:

        GlShader();

        ~GlShader();

        GLuint loadFiles(const string& vertex_file_name, const string& fragment_file_name);
        GLuint load(const string& vertex_source, const string& fragment_source);
        GLuint load(const GLchar* vertex_source, const GLchar* fragment_source);

        void use();

        GLuint getIDProgram() { return ShaderProgram; }

        bool isLoad() { return ShaderProgram != 0; }

        //! Attribute
        GLint getAttribLocation(const GLchar* name) const;
        GLint getAttribLocation(const std::string& name) const;
        //! Uniform get
        GLint getUniformLocation(const GLchar* name) const;
        GLint getUniformLocation(const std::string& name) const;
        //! Uniform set
        void setUniform(GLint location, const vec4& value);
        void setUniform(GLint location, const vec3& value);
        void setUniform(GLint location, const vec2& value);

        void setUniform(GLint location, const mat4& value);
        void setUniform(GLint location, const GLint value);
        private:
        //! Функции печати лога шейдера
        void printInfoLogShader(GLuint shader);
        //! Функция печати лога шейдерной программы
        void printInfoLogProgram(GLuint shader);

        GLuint loadSourcefile(const string& source_file_name, GLuint shader_type);

        GLuint compileSource(const GLchar* source, GLuint shader_type);

        void linkProgram();

        GLuint ShaderProgram;
        GLuint vertex_shader;
        GLuint fragment_shader;
};

#endif



GlShader.cpp:

/*http://esate.ru, Flashhell*/


#include "GlShader.h"
#include 
#include 

using std::ifstream;

GlShader::GlShader():ShaderProgram(0)
{

}

GlShader::~GlShader()
{
  glUseProgram(0);
  glDeleteShader(vertex_shader);
  glDeleteShader(fragment_shader);
  glDeleteProgram(ShaderProgram);
}

GLuint GlShader::loadFiles(const string& vertex_file_name,const string& fragment_file_name)
{
  vertex_shader = loadSourcefile(vertex_file_name, GL_VERTEX_SHADER);
  fragment_shader = loadSourcefile(fragment_file_name, GL_FRAGMENT_SHADER);

  linkProgram();

  return ShaderProgram;
}

GLuint GlShader::load(const string& vertex_source,const string& fragment_source)
{
  vertex_shader   = compileSource(vertex_source.c_str(), GL_VERTEX_SHADER);
  fragment_shader = compileSource(fragment_source.c_str(), GL_FRAGMENT_SHADER);

  linkProgram();
  return ShaderProgram;
}

GLuint GlShader::load(const GLchar* vertex_source, const GLchar* fragment_source)
{
  vertex_shader   = compileSource(vertex_source, GL_VERTEX_SHADER);
  fragment_shader = compileSource(fragment_source, GL_FRAGMENT_SHADER);

  linkProgram();
  return ShaderProgram;
}

void GlShader::linkProgram()
{
  GLint link_ok = GL_FALSE;
  if(!vertex_shader || !fragment_shader)
  {
    ShaderProgram = GL_FALSE;
    return;
  }

  ShaderProgram = glCreateProgram();
  glAttachShader(ShaderProgram, vertex_shader);
  glAttachShader(ShaderProgram, fragment_shader);

  glLinkProgram(ShaderProgram);
  glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &link_ok);
  if (!link_ok) {
    std::cout<<"glLinkProgram:";
    #ifdef _DEBUG
      printInfoLogProgram(ShaderProgram);
    #endif
    ShaderProgram = GL_FALSE;
    return;
  }
}

void GlShader::use()
{
  glUseProgram(ShaderProgram);
}

//---------------------------------------------------

//! Attribute get
GLint GlShader::getAttribLocation(const GLchar* name) const
{
  GLint location = -1;
  location = glGetAttribLocation(ShaderProgram, name);
  if (location == -1) 
    std::cout<<"Could not bind attribute "< 1) 
  {  
    GLchar *infoLog = new GLchar[infologLen]; 
    if (infoLog == NULL) 
    { 
      std::cout<<"ERROR: Could not allocate InfoLog buffer\n"; 
      exit(1); 
    } 
    glGetShaderInfoLog(shader, infologLen, &charsWritten, infoLog);

    std::cout<<"InfoLog: "< 1) 
  {  
    GLchar *infoLog = new GLchar[infologLen]; 
    if (infoLog == NULL) 
    { 
      std::cout<<"ERROR: Could not allocate InfoLog buffer\n"; 
      exit(1); 
    } 
    glGetProgramInfoLog(shader, infologLen, &charsWritten, infoLog);

    std::cout<<"InfoLog: "<  begin(file),  end;
        string  sourceStr(begin,  end);
  file.close();
  return  compileSource(sourceStr.c_str(),  shader_type);
}


Это просто обертка вокруг шейдеров, которую мы уже научились использовать в прошлом уроке, она помогает выполнить часть рутинной работы. Остановимся на некоторых моментах:

printInfoLogShader
printInfoLogProgram

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


Также у нас появилась возможность держать шейдеры не только в коде, но и в отдельных файлах и загружать их с помощью метода loadFiles, который принимает имена (пути) для вершинного и фрагметного щейдеров на вход.

Также появилось много методов для получения и установления значений юниформов и атрибутов. Методы принимают векторы и матрицы из библиотеки GLM, которую мы вкратце рассмотрим по ходу урока.

Теперь, когда у нас есть класс шейдоров, мы можем приступить к изменениям непосредственно в основном коде.

Сначала добавим файл для работы с матрицами из библиотеки GLM.

/*http://esate.ru, Flashhell*/

#include "glm/gtc/matrix_transform.hpp"

Теперь подключим наш только что созданный класс шейдера.

/*http://esate.ru, Flashhell*/

#include "GlShader.h"


Создадим сам шейдер, чуть ниже includ-ов.

/*http://esate.ru, Flashhell*/

GlShader shader;


Уберем все VBO и шейдерные идентификаторы, вместо них используем следующие:

/*http://esate.ru, Flashhell*/


//! ID атрибута вершин
GLint  Attrib_vertex;
//! ID атрибута цветов
GLint  Attrib_color;
//! ID юниформ матрицы проекции
GLint  Unif_matrix;
//! ID Vertex Buffer Object
GLuint VBO_vertex;
//! ID Vertex Buffer Object
GLuint VBO_color;
//! ID VBO for element indices
GLuint VBO_element;



Добавим несколько необходимых переменных:

/*http://esate.ru, Flashhell*/


//! Количество индексов
GLint Indices_count;
//! Матрица проекции
mat4 Matrix_projection;


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

mat4 Matrix_projection – это матрица 4x4 из библиотеки GLM, в данном коде мы храним матрицу проекции, куда в дальнейшем запишем построенную перспективную матрицу проекции.


Теперь наша вершина трехкомпонентная, к ней добавился Z компонент.

/*http://esate.ru, Flashhell*/


//! Вершина
struct vertex
{
  GLfloat x;
  GLfloat y;
  GLfloat z;
};


Функцию shaderLog удаляем, так как у нас уже есть данный функционал в классе шейдера.

В initGL добавим строчку для включения теста глубины. Тест глубины нужен для того, чтобы OpenGL в правильном порядке выводил пиксели рисуемого объекта. То есть чтобы учитывалась глубина (Z), а не порядок отрисовки примитивов в коде.

/*http://esate.ru, Flashhell*/


void initGL()
{
  glClearColor(0, 0, 0, 0);
  glEnable(GL_DEPTH_TEST);
}



checkOpenGLerror остается без изменений.

А вот инициализация шейдеров претерпела большие изменения:

/*http://esate.ru, Flashhell*/


//! Инициализация шейдеров
void initShader()
{
  //! Исходный код шейдеров
  const GLchar* vsSource = 
    "attribute vec3 coord;\n"
    "attribute vec3 color;\n"
    "varying vec3 var_color;\n"
    "uniform mat4 matrix;\n"
    "void main() {\n"
    "  gl_Position = matrix * vec4(coord, 1.0);\n"
    "  var_color = color;\n"
    "}\n";
  const GLchar* fsSource = 
    "varying vec3 var_color;\n"
    "void main() {\n"
    "  gl_FragColor = vec4(var_color, 1.0);\n"
    "}\n";
  
  if(!shader.load(vsSource, fsSource))
  {
    std::cout << "error load shader \n";
    return;
  }

  ///! Вытягиваем ID атрибута из собранной программы 
  Attrib_vertex = shader.getAttribLocation("coord");

  //! Вытягиваем ID юниформ
  Attrib_color = shader.getAttribLocation("color");

  //! Вытягиваем ID юниформ матрицы проекции
  Unif_matrix = shader.getUniformLocation("matrix");

  checkOpenGLerror();
}


Код сократился благодаря вспомогательному классу, но здесь также изменились шейдеры и получаемые из них идентификаторы.

Остановимся подробно на некоторых моментах в шейдерах:

Шейдеры

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

attribute vec3 color – это атрибут цвета, точно такой же как и у вершин.

varying vec3 var_color – переменная с цветом.

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

В нашем случаи, мы ее используем в строке:

var_color = color;

сolor - это атрибут, получаемый шейдером; он будет содержать цвет вершины из массива цветов.

var_color – это уже переменная, значение которой будет доступно во фрагментном шейдере.

Когда значение varying переменной будет попадать из вершинного шейдера в фрагментный, значение интерполируется по примитиву. Это значит, что если у нас, к примеру, есть две вершины линии, и одна вершина имеет черный цвет (R: 0.0, G: 0.0, B: 0.0), а другая белый (R: 1.0, G: 1.0, B: 1.0), то между ними, посередине линии, будет серый цвет (R: 0.5, G: 0.5, B: 0.5):

Уроки OpenGL различных тематик: серый цвет между черной и белой вершиной

/*http://esate.ru, Flashhell*/

gl_Position = matrix * vec4(coord, 1.0);

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

Фрагментный шейдер:

В фрагментном шейдере просто записываем полученный из вершинного шейдера интерполированный цвет:

/*http://esate.ru, Flashhell*/

gl_FragColor = vec4(var_color, 1.0);


Далее используем наш класс для загрузки шейдеров и получения ID юниформ и атрибутов:

/*http://esate.ru, Flashhell*/


if(!shader.load(vsSource, fsSource))
  {
    std::cout << "error load shader \n";
    return;
  }

  Attrib_vertex = shader.getAttribLocation("coord");
…


Далее по коду мы используем наш класс для загрузки шейдеров и получения ID юниформ и атрибутов:
Затем изменим инициализацию и создание VBO:

/*http://esate.ru, Flashhell*/


//! Инициализация VBO_vertex
void initVBO()
{
  //! Вершины куба
  vertex vertices[] = { 
      {-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}
  };
  //! Цвета куба без альфа компонента(RGB)
  vertex colors[] = { 
      {1.0f, 0.5f, 1.0f},
      {1.0f, 0.5f, 0.5f},
      {0.5f, 0.5f, 1.0f},
      {0.0f, 1.0f, 1.0f},
      {1.0f, 0.0f, 1.0f},
      {1.0f, 1.0f, 0.0f},
      {1.0f, 0.0f, 1.0f},
      {0.0f, 1.0f, 1.0f}
  };
  //! Индексы вершин, общие и для цветов
  GLint indices[] = {
    0, 4, 5, 0, 5, 1,
    1, 5, 6, 1, 6, 2,
    2, 6, 7, 2, 7, 3,
    3, 7, 4, 3, 4, 0,
    4, 7, 6, 4, 6, 5,
    3, 0, 1, 3, 1, 2
  };

  // Создаем буфер для вершин
  glGenBuffers(1, &VBO_vertex);
  glBindBuffer(GL_ARRAY_BUFFER, VBO_vertex);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  // Создаем буфер для цветов вершин
  glGenBuffers(1, &VBO_color);
  glBindBuffer(GL_ARRAY_BUFFER, VBO_color);
  glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

  // Создаем буфер для индексов вершин
  glGenBuffers(1, &VBO_element);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO_element);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

  Indices_count = sizeof(indices) / sizeof(indices[0]);

  checkOpenGLerror();
}

void freeVBO()
{
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  
  glDeleteBuffers(1, &VBO_element);
  glDeleteBuffers(1, &VBO_element);
  glDeleteBuffers(1, &VBO_color);
}


Как известно, у куба 6 сторон и 8 вершин. В старых версиях OpenGL мы могли бы нарисовать каждую сторону с помощью GL_QUAD (S), но так как мы ориентируемся только на новые версии OpenGL и OpenGL ES, то вынуждены рисовать каждую сторону треугольниками. Потому как GL_QUAD(S) были из них удалены. То есть на каждую сторону приходится два треугольника, что означает всего 12 треугольников.


Если использовать glDrawArrays нам придется записать огромный массив обхода вершин из повторяющихся данных.
Уроки OpenGL различных тематик: пример визуализации куба из 8 вершин, соединенных полигонами Рисунок 1. Пример визуализации куба из 8 вершин, соединенных полигонами.
Но мы пошли другим путем и записали в один массив уникальные 8 вершин (vertices) и создали другой массив, с индексами (indices) обхода первого массива. Также у нас есть массив цветов для каждой вершины, здесь тоже совершается обход из массива индексов.

Создание VBO для массива индексов ничем не отличается от его создания для вершин. Меняется только один параметр на GL_ELEMENT_ARRAY_BUFFER:

/*http://esate.ru, Flashhell*/


glGenBuffers(1, &VBO_element);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO_element);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);


Далее записывается количество элементов в массиве индексов:

/*http://esate.ru, Flashhell*/

Indices_count = sizeof(indices) / sizeof(indices[0]);


Это необходимо для дальнейшего указания этого значения функции отрисовки.

freeVBO особо не изменилась, просто освободились дополнительные буферы.

/*http://esate.ru, Flashhell*/


void resizeWindow(int width, int height)
{
  glViewport(0, 0, width, height);

  height = height > 0 ? height : 1;
  const GLfloat aspectRatio = (GLfloat)width/(GLfloat)height;

  Matrix_projection = glm::perspective(45.0f, aspectRatio, 1.0f, 200.0f);
  // Перемещаем центр нашей оси координат для того чтобы увидеть куб
  Matrix_projection = glm::translate(Matrix_projection, vec3(0.0f, 0.0f, -10.0f));
  // Поворачиваем ось координат(тоесть весь мир), чтобы развернуть отрисованное
  Matrix_projection = glm::rotate(Matrix_projection, 60.0f, vec3(1.0f, 1.0f, 0.0f));
}


Здесь же есть очень интересные изменения. Во-первых, здесь мы строим матрицу проекции с помощью GLM-функции Perspective (так как мы отказались от GLU из-за использования ею удаленных функций), во вторых, отодвигаем и поворачиваем эту матрицу, для того чтобы увидеть наш куб.

Перейдем к главной функции, функции рисования:

/*http://esate.ru, Flashhell*/

void render()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  //! Устанавливаем шейдерную программу текущей
  shader.use();
  //! Передаем матрицу в шейдер
  shader.setUniform(Unif_matrix, Matrix_projection);
  
  //! Подключаем буфер с индексами вершин общий для цветов и их вершин
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO_element);

  //! ВЕРШИНЫ
  //! Включаем массив атрибутов для вершин
  glEnableVertexAttribArray(Attrib_vertex);
    //! Подключаем VBO
    glBindBuffer(GL_ARRAY_BUFFER, VBO_vertex);
      //! Указывая pointer 0 при подключенном буфере, мы указываем что данные в VBO
      glVertexAttribPointer(Attrib_vertex, 3, GL_FLOAT, GL_FALSE, 0, 0);

  //! ЦВЕТА
  //! Включаем массив атрибутов для цветов
  glEnableVertexAttribArray(Attrib_color);
    glBindBuffer(GL_ARRAY_BUFFER, VBO_color);
      glVertexAttribPointer(Attrib_color, 3, GL_FLOAT, GL_FALSE, 0, 0);


  //! Передаем данные на видеокарту(рисуем)
  glDrawElements(GL_TRIANGLES, Indices_count, GL_UNSIGNED_INT, 0);

  //! Отключаем массив атрибутов
  glDisableVertexAttribArray(Attrib_vertex);

  //! Отключаем массив атрибутов
  glDisableVertexAttribArray(Attrib_color);

  checkOpenGLerror();

  glutSwapBuffers();
}


glClear теперь очищает и буфер глубины. Для этого ей передается параметр GL_DEPTH_BUFFER_BIT.

Далее используем наш класс шейдера и передаем ему матрицу проекции в качестве юниформа. Подключаем буфер массивом индексов, так же как и обычный буфер VBO, но лишь с другим параметром (GL_ELEMENT_ARRAY_BUFFER).

Подключив буферы с цветами и вершинами, вызываем метод рисования glDrawElements, который при этом будет использовать наш массив индексов. Последний параметр, как в случае с VBO, мы указываем нулем, что означает команду "брать их из VBO (VBO_element)", подключённого ранее.

Функцию freeShader мы удаляем, так как теперь наш класс сам выполнит эту работу. А main осталась такая же, только лишилась удаленной функции freeShader.

В конце концов, после компиляции и запуска программы вы должны увидеть следующее:

Уроки OpenGL различных тематик: Результат работы программы: визуализация разноцветного куба в перспективной проекции с помощью шейдеров. Рисунок 2. Результат работы программы: визуализация разноцветного куба в перспективной проекции с помощью шейдеров.

Эпилог

Итак, в данном уроке вы познакомились с тем, как построить проекцию и как использовать varying переменные в шейдерах, научились использовать множество VBO и массивы индексов, разобрались, для чего они нужны.

Проекты:

Скачать проект (Visual studio 2012)
Скачать проект (CodeLite)

Полезные ссылки:

Сайт библиотеки GLEW
Сайт библиотеки freeglut
Материалы по GLSL clockworkcoders
Материалы по GLSL lighthouse3d
Материалы по OpenGL 3.3 opengl-tutorial
Материалы на GPWiki

Книги:

Разработка и отладка шейдеров. Алексей Боресков
OpenGL 4.0 Shading Language Cookbook
OpenGL суперкнига 5-тое издание

Добавить комментарий
Расширенный режим добавления комментариев доступен на форуме: загрузка изображений, цитирование, форматирование текста, и т.д.
Ваше имя:
Текст сообщения:
Комментарии (2):
Alexey Kamanin
Alexey Kamanin,  
Спасибо за материал.
Но вот беда, ссылки на скачивание не работают.
badcat
badcat,  
Привет! Соберал этот код в солюшен, вот ссылка, может еще кому пригодится.
^