Внимание!

Эта публикация перенесена в раздел уроков по адресу OpenGL шейдеры. Простой шейдер на GLSL.
К ней прикреплена новая отдельная ветка комментариев форума, которую вы можетет найти после текста публикации.
Обсуждение публикации рекуомендуется вести по новому адресу, который указан выше.

OpenGL шейдеры. Простой шейдер на GLSL

Выходят новые версии OpenGL. И все время, появляется информация, о том, что какие-то функции уже не рекомендуемые,а какие-то удалены. Фиксированный графический конвейер(ФГК) уходит, с новыми версиями OpenGL. Точнее он уже ушел, в OpenGL 3.3, core profile его уже нет. А, что же тогда приходит на смену традиционному, старому функционалу?!
[spoiler]

А ничего, многое к сожалению, убрали из-за неэффективности. К примеру, весь функционал с матричными операциями (glPushMatrix,glPopMatrix...) и старые добрые glBegin, glEnd, ушли из основного функционала, и доступны лишь в compatibility profile. Теперь без шейдеров, в core profile не отрисуешь, и пикселя. И желательно хранить вершины и их атрибуты в различных буферах на GPU.

Но не стоит отчаиваться! То, что делалось раньше, выполнимо и сейчас, лишь несколько иначе. Просчитывать матрицы теперь придется в "ручную" или с помощью сторонних библиотек. А рендерить придется с помощью щейдеров, что в прочем, не так страшно. Ведь много чего интересного можно выполнить с помощью шейдеров, то что никогда не получилось бы используя только фиксированный графический конвейер. Стоит только слегка подучится их писать, использовать. И можно будет сделать: свою модель освещения, произвести видовые преобразования, самому интерпретировать данные с текстур... Теперь многие операции с вершинами и пикселями должны производится в шейдерах. И стоит отметить, что правильно написанное приложение использующее OpenGL 3.3, намного производительнее того же по функционалу приложения написанного на OpenGL до 2.0 и использующие лишь ФГК. Еще бы! Ведь большая часть функции в шейдерах аппаратно ускоренны!

Хотя в, принципе шейдеры можно было использовать начиная уже с 2.0. Тогда, возникает вопрос: "почему ФГК не убрали тогда?!". Да потому, что тогда все написанные приложение были бы не совмести с новыми API OpenGL. Да, и сейчас можно писать используя ФГК OpenGL в compatibility profile режиме. Который, как раз и существует ради того, чтоб старые приложения работали. Многие и сейчас используют совместимый режим, так, как много учебников, и туториалов досталось нам с тех времен, когда был лишь ФГК. И начав изучать OpenGL большая часть программистов начинают именно со старых версий с ФГК. Так и выходит, что старый, фиксированный функционал был и остается для многих привычнее и удобнее.

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

И собравшись с силами, наконец чуток их осилил=) И теперь решил поделится самым простым примером их использования, да именно, самым, что ни есть простым. Потому, как мне, при знакомстве с учебными материалами посвященными шейдерам, не хватало именно простых примеров. И я решил, что лучшем решением будет начать с самого простого.

Теоретический экскурс

Как вы поняли речь пойдет о GLSL шейдерах, хотя GLSL не единственный шейдерный язык который может работать с OpenGL. С OpenGL можно также использовать шейдеры на Cg(C for Graphics). Но мне больше по душе родной для OpenGL шейдерный язык.
Примечание для их использования требуются дополнительная библиотека cg(от NVidia).

GLSL(OpenGL Shading Language) — язык высокого уровня для программирования шейдеров. Синтаксис языка базируется на языке программирования ANSI C, однако, из-за его специфической направленности, из него были исключены многие возможности, для упрощения языка и повышения производительности. В язык включены дополнительные функции и типы данных, например для работы с векторами и матрицами. GLSL стал полностью частью OpenGL в версии 2.0.

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

Шейдерная программа - это не большая программа состоящая из шейдеров(вершинного и фрагментного, возможно и не только) выполняющаяся на GPU(graphics processing unit), тоесть на графическом процессоре видеокарты.


Графический конвейер OpenGL 2.0



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

Геометрический шейдер- шейдер способный обработать не только одну вершину, но и целый примитив. Он может либо отбросить(от обработки) примитивы, либо создать новые, то есть геометрический шейдер способен генерировать примитивы. А также способен изменять тип входных примитивов. (Примечание: геометрически шейдер полностью вошел в OpenGL, в версии 3.2)

Фрагментный шейдер - заменяет часть графического конвейера(ГК), обрабатывающих каждый полученный на предыдущих стадиях ГК фрагмент(не пиксель). Обработка может включать такие стадии, как получение данных из текстуры, просчет освещения, просчет смешивания.
Обязательной работой для фрагментного шейдера является запись цвета фрагмента, в встроенную переменную gl_FragColor, или его отбрасывание специальной командой discard. В случаи отбрасывания фрагмента, никакие расчеты дальше с ним производится не будут, и фрагмент уже не попадет в буфер кадра.

(Примечание: также в OpenGL есть еще два типа тесселяционных шейдеров, они доступны в OpenGL 4.0 и выше)


Загрузка и компиляция

GLSL шейдеры принято хранить в виде исходных кодов(хотя в OpenGL 4.1 и появилась возможность загружать шейдеры в виде бинарных данных). Такой подход был использован, для лучшей переносимости шейдеров на различные аппаратные и программные платформы. Исходные коды компилируются драйвером. Они могут быть скомпилированы лишь после создания действующего контекста OpenGL. Драйвер сам генерирует внутри себя оптимальный двоичный код, который понимает данное оборудование. Это гарантирует, что один и тот же шейдер будет правильно и эффективно работать на различных платформах.

Исходный код может быть представлен в виде ANSI строк завершающихся переносом строки ('\n') или без него. В случаи если переноса нет, нужно передать массив длин каждой строки.

Шаги загрузки и компиляции:

-Сначала выделяются идентификаторы в виде GLuint, под шейдеры - glCreateShader и шейдерную программу glCreateProgram.
- На идентификатор шейдера загружается исходный код, который передается драйверу glShaderSource.
- После шейдер компилируется glCompileShader.
- Несколько шейдеров разных типов, прикрепляются к программе glAttachShader
- Последний шаг линкование прикрепленных шейдеров в одну шейдерную программу glLinkProgram


Практика=)

Пример:
А сейчас рассмотрим небольшой пример, который использует OpenGL 2.0. Но при этом в примере не используется фиксированный графический конвейер, чтобы максимально приблизится, к OpenGL 3.3. Это может помочь более плавному переходу на новые версии OpenGL, а также чтобы было легче работать с OpenGL ES 2.0/3.0, так как в OpenGL ES 2.0/3.0 также отсутствует фиксированный графический конвейер.
Мы будем использовать вершинный и фрагментный шейдеры, так без них в современных версиях OpenGL ничего не нарисуешь, а остальные типы шейдеров мы пока рассматривать не будем, так их нет в OpenGL 2.0, и они не являются обязательными в OpenGL 3.3 и выше.

Шейдеры
Вершинный шейдер
/*http://esate.ru, Flashhell*/

attribute vec2 coord;
void main() {
gl_Position = vec4(coord, 0.0, 1.0);
}

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

attribute vec2 coord;
Создаем атрибут в виде двухмерного вектора с именем coord. Именно в нем и будут приходить данные о координатах вершины.
Атрибут(attribute) - это данные передаваемые программой вершинному шейдеру(другим шейдерам данные не доступны). Причем данные приходят шейдеру на каждую вершину. Эти данные доступны только для чтения.
vec2 - это двумерный вектор типа float.

void main() - вход в программу.

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

gl_Position = vec4(coord, 0.0, 1.0);
gl_Position - это встроенная переменная для записи обработанной шейдером позиции вершины. Так, как она имеет тип vec4, мы создаем вектор из четырех компонент беря x и y из атрибута, который является двумерным вектором, z ставим в ноль, а w в 1.0. Далее наши данные об вершине идут дальше по конвейеру.


Фрагментный шейдер
/*http://esate.ru, Flashhell*/

uniform vec4 color;
void main() {
gl_FragColor = color;
}

uniform vec4 color;
Объявляем юниформ переменную типа четырех компонентного вектора. В ней мы передадим желаемый цвет примитива.

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

gl_FragColor - это встроенная переменная имеющая тип vec4, в нее записывается обработанный фрагментным шейдером цвет фрагмента.

Для сборки примера, Вам понадобится библиотеки GLEW и freeglut

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

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

#include "include/GL/glew.h"
#include "include/GL/glut.h"

#include <iostream>

//! Переменные с индентификаторами ID
//! ID шейдерной программы
GLuint Program;
//! ID атрибута
GLint  Attrib_vertex;
//! ID юниформ переменной цвета
GLint  Unif_color;
//! ID Vertex Buffer Object
GLuint VBO;

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

//! Функция печати лога шейдера
void shaderLog(unsigned int shader) 
{ 
  int   infologLen   = 0;
  int   charsWritten = 0;
  char *infoLog;

  glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infologLen);

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

//! Инициализация OpenGL, здесь пока по минимальному=)
void initGL()
{
  glClearColor(0, 0, 0, 0);
}

//! Проверка ошибок OpenGL, если есть то выводж в консоль тип ошибки
void checkOpenGLerror()
{
  GLenum errCode;
  if((errCode=glGetError()) != GL_NO_ERROR)
   std::cout << "OpenGl error! - " << gluErrorString(errCode);
}

//! Инициализация шейдеров
void initShader()
{
  //! Исходный код шейдеров
  const char* vsSource = 
   "attribute vec2 coord;\n"
   "void main() {\n"
   "  gl_Position = vec4(coord, 0.0, 1.0);\n"
   "}\n";
  const char* fsSource = 
   "uniform vec4 color;\n"
   "void main() {\n"
   "  gl_FragColor = color;\n"
   "}\n";
  //! Переменные для хранения идентификаторов шейдеров
  GLuint vShader, fShader;
  
  //! Создаем вершинный шейдер
  vShader = glCreateShader(GL_VERTEX_SHADER);
  //! Передаем исходный код
  glShaderSource(vShader, 1, &vsSource, NULL);
  //! Компилируем шейдер
  glCompileShader(vShader);

  std::cout << "vertex shader \n";
  shaderLog(vShader);

  //! Создаем фрагментный шейдер
  fShader = glCreateShader(GL_FRAGMENT_SHADER);
  //! Передаем исходный код
  glShaderSource(fShader, 1, &fsSource, NULL);
  //! Компилируем шейдер
  glCompileShader(fShader);

  std::cout << "fragment shader \n";
  shaderLog(fShader);

  //! Создаем программу и прикрепляем шейдеры к ней
  Program = glCreateProgram();
  glAttachShader(Program, vShader);
  glAttachShader(Program, fShader);

  //! Линкуем шейдерную программу
  glLinkProgram(Program);

  //! Проверяем статус сборки
  int link_ok;
  glGetProgramiv(Program, GL_LINK_STATUS, &link_ok);
  if(!link_ok)
  {
   std::cout << "error attach shaders \n";
   return;
  }
  ///! Вытягиваем ID атрибута из собранной программы 
  const char* attr_name = "coord";
  Attrib_vertex = glGetAttribLocation(Program, attr_name);
  if(Attrib_vertex == -1)
  {
   std::cout << "could not bind attrib " << attr_name << std::endl;
   return;
  }
  //! Вытягиваем ID юниформ
  const char* unif_name = "color";
  Unif_color = glGetUniformLocation(Program, unif_name);
  if(Unif_color == -1)
  {
   std::cout << "could not bind uniform " << unif_name << std::endl;
   return;
  }

  checkOpenGLerror();
}

//! Инициализация VBO
void initVBO()
{
  glGenBuffers(1, &VBO);
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  //! Вершины нашего треугольника
  vertex triangle[3] = { 
   {-1.0f,-1.0f},
   { 0.0f, 1.0f},
   { 1.0f,-1.0f}
  };
  //! Передаем вершины в буфер
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);

  checkOpenGLerror();
}

//! Освобождение шейдеров
void freeShader()
{
  //! Передавая ноль, мы отключаем шейдрную программу
  glUseProgram(0); 
  //! Удаляем шейдерную программу
  glDeleteProgram(Program);
}

//! Освобождение шейдеров
void freeVBO()
{
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glDeleteBuffers(1, &VBO);
}

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

//! Отрисовка
void render()
{
  glClear(GL_COLOR_BUFFER_BIT);
  //! Устанавливаем шейдерную программу текущей
  glUseProgram(Program); 
  
  static float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
  //! Передаем юниформ в шейдер
  glUniform4fv(Unif_color, 1, red);

  //! Включаем массив атрибутов
  glEnableVertexAttribArray(Attrib_vertex);
   //! Подключаем VBO
   glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //! Указывая pointer 0 при подключенном буфере, мы указываем что данные в VBO
    glVertexAttribPointer(Attrib_vertex, 2, GL_FLOAT, GL_FALSE, 0, 0);
   //! Отключаем VBO
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   //! Передаем данные на видеокарту(рисуем)
   glDrawArrays(GL_TRIANGLES, 0, sizeof (vertex));

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

  //! Отключаем шейдерную программу
  glUseProgram(0); 

  checkOpenGLerror();

  glutSwapBuffers();
}

int main( int argc, char **argv )
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_ALPHA | GLUT_DOUBLE);
  glutInitWindowSize(800, 600);
  glutCreateWindow("Simple shaders");

  //! Обязательно перед инициализации шейдеров
  GLenum glew_status = glewInit();
  if(GLEW_OK != glew_status) 
  {
    //! GLEW не проинициализировалась
   std::cout << "Error: " << glewGetErrorString(glew_status) << "\n";
   return 1;
  }

  //! Проверяем доступность OpenGL 2.0
  if(!GLEW_VERSION_2_0) 
   {
    //! OpenGl 2.0 оказалась не доступна
   std::cout << "No support for OpenGL 2.0 found\n";
   return 1;
  }

  //! Инициализация
  initGL();
  initVBO();
  initShader();
  
  glutReshapeFunc(resizeWindow);
  glutDisplayFunc(render);
  glutMainLoop();
  
  //! Освобождение ресурсов
  freeShader();
  freeVBO();
}

Результат работы:




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

Я по максимуму постарался комментировать код, но некоторые аспекты я не осветил, к примеру здесь использовались VBO(Vertex Buffer Object), связанно это с тем, что хотелось максимально избавится от устаревших функции в новых версиях OpenGL. Но статья и так растянулась, поэтому я решил пока опустить освещение этих аспектов. Я постараюсь написать про VBO, если все таки про VBO не напишет Isaer. Но если что не понятно не стесняйтесь спросить.

Большое спасибо за помощь и критику, mr.Isaer. Благодаря ему я написал столько много, хотя изначально, в первой версии статьи почти ничего кроме кода не было=)


Скачать проект

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

Сайт библиотеки GLEW
Сайт библиотеки freeglut
Туториалы по GLSL clockworkcoders
Туториалы по GLSL lighthouse3d


Книги:

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

Внимание!

Эта публикация перенесена в раздел уроков по адресу OpenGL шейдеры. Простой шейдер на GLSL.
К ней прикреплена новая отдельная ветка комментариев форума, которую вы можетет найти после текста публикации.
Обсуждение публикации рекуомендуется вести по новому адресу, который указан выше.

0  
01.09.2012 00:00:00
Про VBO бы узнать подробнее…
А так вроде про шейдеры, что стало ясно.
0  
02.09.2012 00:00:00
glGetUniformLocation что не понял назначения.
glUniform4fv(Unif_color, 1, red); и что это делает???
0  
02.09.2012 00:00:00
glGetUniformLocation — это как я понял достает из шейдера переменную
glUniform4fv(Unif_color, 1, red); — заносит в нее значение
0  
02.09.2012 00:00:00
блин, зачем??? не надо было этого писать =))) ну ладно уже) пасиб

про vbo пишу, уже почти есть, сейчас запостил VA а с VBO будет полезно знать и VA, так что скоро
0  
22.12.2012 00:00:00
Зачем писать coord.xy, если coord двумерный вектор?
Можно просто coord написать…
0  
22.12.2012 00:00:00
Вы правы можно просто написать coord.
Просто эта переделка, кода, где использовался vec4, наверное из за этого, так осталось написано=) сейчас поправлю
0  
21.01.2013 00:00:00
А когда будет продолжение? Хотелось, что то по сложнее и интереснее чем просто треугольник. В 3D, с текстурами, освещением, картами всякими…
0  
11.11.2016 12:41:15
Здравствуйте, обновите, пожалуйста ссылку на файл с проектом, либо укажите, где ее можно скачать, чтобы приложить к уроку.
(dl.dropbox.com/u/47221662/Simple_Shaders.zip - не доступна)

esate.ru/forum/?PAGE_NAME=message&fid=2&tid=70&TITLE_SEO=70-opengl-sheydery.-prostoy-sheyder-na-glsl&mid=508#message508
0  
09.05.2017 04:47:09
К сожалению, файлы с проектом были утеряны. Можно просто собрать новый проект, скопировав код в файлы, вернее всего в один файл.
^