Shaders in WebGL

Автор: | 22.12.2018

Шейдер (от  англ.  Shader) –  небольшая  программа, выполняемая  на  стороне  видеокарты (GPU),  которая  позволяет  производить  отдельные  элементы  цикла рендеринга объекта особым, отличным от стандартного, образом. Иначе говоря, шейдер – программа, выполняющая некоторую часть цикла рендеринга.

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

Ниже приводятся 2-а варианта  фрагментов кода прорисовки раскрашенного в зеленый цвет треугольника. Слева представлен код для фиксированного  конвейера, справа – код  с вершинным и  фрагментным (пиксельным) шейдерами. Устаревшие команды glVertex между glBegin/glEnd а также команды glLoadIdentity,  glTranslatef, glRotatef и glScalef использованы в примере для простоты понимания процесса.

Шейдерные программы пишутся на специально разработанных языках, одним из которых является язык GLSL(OpenGL Shading Language), который полностью стал частью OpenGL начиная с версии 2.0. Синтаксис GLSL основан на языках программирования семейства C, Он включает   почти  все  распространенные  в  С/С++  арифметические, логические и битовые операторы, конструкторы для инициализации и прочее.

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

Вершинные  шейдеры  предназначены для обработки  каждой  вершины  и нахождения  её  координат  с  учетом  матриц  модели и проекций, а также прочих,  зависящих  от задумки  программиста,  условий. Для  этого  вершинный шейдер  получает  все  параметры вершины (координаты,  цвет,  текстурные  координаты  и  т.д.). Также  основная  программа может  передать  шейдеру  любые  другие,  определяемые  программистом  параметры, включая  совершенно  произвольные,  имеющие  смысл  только  для  выполнения  общей задачи.  Например, шейдеру  может  быть  передан  параметр «сила  ветра»,  который совершенно не привязан ни к какому способу представления трехмерных объектов, ни к какому  графическому API.  Это  просто  параметр,  который  будет  обрабатываться  внутри шейдера.

В примере единственная  строка  вершинного  шейдера рассчитывает  координаты  трехмерной  вершины  и  заносит  их  в  переменную  типа vec4 gl_Position.  Это  происходит  путем  перемножения  вектора gl_Vertex,  содержащего координаты вершины, и матрицы gl_ModelViewProjectionMatrix.

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

В примере одной  строчкой  мы  определяем цвет  в  переменной gl_FragColor.  Таким образом, мы рассчитываем цвет текущего пикселя.

Шейдеры взаимодействуют с фиксированной частью конвейера OpenGL, записывая значения во встроенные переменные OpenGL, которые имеют префикс «gl_«.  Вершинный шейдер ответственен как минимум за одну переменную: gl_Position, и обычно трансформирует вершину в матрицах проекции и моделей. Обязательной работой для фрагментного шейдера является запись цвета фрагмента, в встроенную переменную gl_FragColor. Вершинный шейдер выполняется один раз для каждой вершины, а фрагментный – один раз для каждого фрагмента. Несколько исполнений одного и того же шейдера могут проходить параллельно.

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

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

Рассмотрим пример компиляции и подключения шейдеров  к основной программе.

void LoadShaderSource(GLhandleARB shader, const char fileName[])
{
  std::string shaderSrc = LoadTextFile(fileName);
  const char* shaderstring = shaderSrc.c_str();
  const char** pshaderstring = &shaderstring;
  glShaderSourceARB(shader, 1, pshaderstring, NULL);
}
GLhandleARB vxShader, frShader, prObject;
vxShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
frShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
LoadShaderSource(vxShader, vxFileName);
LoadShaderSource(frShader, frFileName);
glCompileShaderARB(vxShader);
glCompileShaderARB(frShader);
prObject = glCreateProgramObjectARB();
glAttachObjectARB(prObject, vxShader);
glAttachObjectARB(prObject, frShader);
glLinkProgramARB(prObject);

Здесь vxFileName и frFileName содержат  имена  файлов,  содержащих  исходный  код  соответственно  вершинного  и фрагментного шейдеров. Компиляция  состоит  в  следующем:  сначала  мы  создаем  объекты GL_VERTEX_SHADER_ARB  и GL_FRAGMENT_SHADER_ARB,  затем  связываем  их  с исходным  кодом,  затем  компилируем  каждой  по  отдельности,  создаем  объект GL_PROGRAM_OBJECT_ARB,  соотносим  с  ним  откомпилированные  вершинный  и фрагментный  шейдеры  и  линкуем  этот  объект.  После  этого  программа  может  сделать шейдер активным. Делается это так:

 glUseProgramObjectARB(prObject);

Отключить шейдеры  можно командой:

 glUseProgramObjectARB(0);

Удаляются шейдеры из памяти следующим образом:

glDeleteObjectARB(vxShader);
glDeleteObjectARB(frShader);
glDeleteObjectARB(prObject);

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

bool SetUniformFloat(const char* name, float value)
{
    int loc = glGetUniformLocationARB(prObject, name);
      if (loc < 0)
          {
              return false;
          } 
   glUniform1fARB(loc, value);
   return true;
}
bool SetUniformTexture(const char* name, int texUnit)
{
int loc = glGetUniformLocationARB(prObject, name);
if (loc < 0)
 {
return false;
 }
glUniform1iARB(loc, texUnit);
return true;
} 
bool SetUniformVector4D(const char* name, const CVector4D& value)
{
    int loc = glGetUniformLocationARB(prObject, name);
    if (loc < 0)
       {
          return false;
       }
  glUniform4fvARB(loc, 1, value);
  return true;
}

В параметрах процедур name – строка  с именем параметра, value –  его  значение, texUnit –  номер  текстурного  уровня.  Эти процедуры устанавливают параметры для активного шейдера.

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

 uniform float CoolestTemp;
 uniform float TempRange;
//Переменные-атрибуты, изменяющиеся для каждой вершины
attribute float VertexTemp;
//Изменяющаяся переменная для коммуникации вершинного и
//фрагментного шейдеров
varying float Temperature;
void main()
{
   // Вычислить температуру, которая будет интерполироваться для
   // каждого фрагмента в диапазоне [0.0, 1.0]
   Temperature = (VertexTemp - CoolestTemp) / TempRange;
   /*
   Позиция вершины передается приложением с помощью функции
   glVertex() и может быть прочитана через встроенную переменную
   gl_Vertex. Используйте это значение и текущую видовую матрицу,
   чтобы сообщить растеризатору, где находится вершина
   */
   gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

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

//Переменные, изменяющиеся приложением один раз на примитив
//vec3 обозначает тип-вектор из трех чисел с плавающей точкой
uniform vec3 CoolestColor;
uniform vec3 HottestColor;
//Temperature теперь содержит интерполированную
//величину температуры для каждого фрагмента
varying float Temperature;
void main()
{
   //Получить цвет между самым холодным и самым горячим
   //с помощью встроенной функции mix()
   vec3 color = mix(CoolestColor, HottestColor, Temperature);
   //Создать вектор из 4-х чисел, присоединив значение alpha=1.0
   //и задать получившееся в качестве цвета фрагмент
   gl_FragColor = vec4(color, 1.0);
}

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

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

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

 

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

OpenGL

 

 

 

Автор: Николай Свирневский

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *