OpenGL шейдеры в современной интерпретациии

в 8:23, , рубрики: glsl, OpenGL, shader, Анимация и 3D графика, Программирование, шейдер, метки: , , ,

glsl shaders sample
Новые версии OpenGL не заставляют себя ждать и все время, появляется информация, что некоторые функции уже не рекомендуются, а то и вовсе удалены. А, что же приходит на смену традиционному, привычному функционалу?!
А ничего, все теперь можно с легкостью выполнить на шейдерах. Об этом и пойдет речь далее.

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

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

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

Сегодня речь пойдет пойдет о GLSL-шейдерах, но GLSL не единственный шейдерный язык для работы с OpenGL.
Можно к примеру использовать шейдеры на CG (C for Graphics). Для этого требуются дополнительная библиотека CG от NVidia.

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

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

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

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

OpenGL шейдеры в современной интерпретациии

Типы шейдеров:

Вершинный шейдер — заменяет часть графического конвейера, выполняющего преобразования связанные с данными вершин. Такие, как умножение вершин(также нормалей) на матрицу проекции и моделирования, установка цветов вершин, установка материалов освещения. Он отработает для каждой отрисованной вершин.
Обязательной работой для вершинного шейдера является запись позиции вершины, в встроенную переменную 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 и выше.

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

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

attribute vec2 coord;

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

void main() — вход в программу.
gl_Position = vec4(coord.xy, 0.0, 1.0);

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

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

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

uniform vec4 color;

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

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

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

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

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

Исходный код примера


#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 buffern";
       exit(1);
    }
    glGetShaderInfoLog(shader, infologLen, &charsWritten, infoLog);
    std::cout<< "InfoLog: " << infoLog << "nnn";
    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.xy, 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 foundn";
    return 1;
  }

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

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

Результат работы:
OpenGL шейдеры в современной интерпретациии

Заключение

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

Автор: supasufa

Источник

Поделиться

  1. dima_sdk:

    Ну, вот, всё круто! Один только минус- опять всё на этом косноязычном си! Автор, напиши аналог под дельфи, получишь армию поклонников!)))

  2. aer001:

    А где исходник?
    На dropbox выдаётся фейл!
    :(

* - обязательные к заполнению поля