OpenGL шейдеры: Первая часть. Простой шейдер на GLSL

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

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

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

Примечание: для их использования потребуется дополнительная библиотека Cg (от NVidia).

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

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

Шейдерная программа - это небольшая программа, состоящая из шейдеров (вершинного и фрагментного, возможны и другие) и выполняющаяся на GPU(Graphics Processing Unit), то есть на графическом процессоре видеокарты.
Уроки OpenGL различных тематик: Графический конвейер OpenGL 2.0 Рисунок 1. Графический конвейер OpenGL 2.0.

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

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

  3. Фрагментный шейдер - заменяет часть графического ковейера (ГК), обрабатывая каждый полученный на предыдущих стадиях ГК фрагмент (не пиксель). Обработка может включать такие стадии, как получение данных из текстуры, просчет освещения, просчет смешивания. Обязательной работой для фрагментного шейдера является запись цвета фрагмента во встроенную переменную 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 ставим = 0, а W = 1.0. Затем наши данные о вершине идут дальше по конвейеру.

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

Код:
/*http://esate.ru, Flashhell*/

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


uniform vec4 color; - в этой переменной типа четырехкомпонентного вектора передадим примитиву желаемый цвет.

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

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

Для сборки примера, Вам понадобzтся библиотеки 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();
}


Вот и всё. Результат работы:

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

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

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

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

Сайт библиотеки GLEW
Сайт библиотеки Freeglut
Инструкции по GLSL
Рекомендации по GLSL

Книги:

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

Нет доступа к просмотру комментариев.

^