MyTetra Share
Делитесь знаниями!
Краткое введение в язык шейдеров GLSL на примере фрагментных шейдеров
Время создания: 26.06.2021 14:17
Автор: Xintrea
Текстовые метки: язык, GLSL, шейдер, документация, обзор, введение
Раздел: Компьютер - Программирование - Шейдеры - Шейдеры GLSL
Запись: xintrea/mytetra_syncro/master/base/16247062684f9z8uc3go/text.html на raw.github.com

Аббревиатура GLSL расшифровывается как OpenGL Shading Language, где буква O пропускается, а GL обозначает Graphics Library.


Язык GLSL представляет собой C-подобный язык программирования со встроенной небольшой математической библиотекой. Слово "встроенная" обозначает, что никаких команд вида #include <math.h> делать не нужно, все математические функции доступны по-умолчанию.


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


Кроме того, в этой статье излагаются только начальные сведения о языке шейдеров, знание которых достаточно для написания несложных шейдеров. Такие шейдеры могут пригодиться, например, для One Scene Shader Compo на Demoparty.


Для быстрого понимания работы с фрагментными шейдерами можно воспользоваться web-ресурсом ShaderToy.com или программой Bonzomatic.



Примечание: программа Bonzomatic, по сути, работает именно с языком GLSL в чистом виде. А онлайн-сервис ShaderToy делает дополнительное окружение для работы шейдеров, поэтому в нем обращение к входным данным и формирование выходных данных происходит несколько по-другому, чем на "чистом" GLSL. В этой статье рассматривается стандартное "чистое" окружение GLSL.




Общая структура кода шейдера


Код шейдеров начинается с объявления версии (например, 410 core). Номер версии необходим, чтобы программное окружение, перед запуском шейдера, могло проверить, понимает ли GPU указанную версию языка шейдеров.


Далее следует список входных/выходных переменных и uniform-переменных. Что такое uniform-переменные, будет объяснено ниже по тексту.


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


В общем виде код шейдера (неважно, вершинный это шейдер или фрагментый), выглядит так:



#version номер_версии

in type inVar1;

in type inVar2;

out type outVar;

uniform type anyUniformVar;


void main()

{

...

outVar=...;

}




Базовые типы



Основные базовые типы


Примитивные базовые типы следующие:


int, uint, float, double, bool



Векторные типы


Векторные типы следующие:



  • vecN — вектор по умолчанию из n элементов типа float;
  • bvecN — вектор из n элементов типа bool;
  • ivecN — вектор из n элементов типа int;
  • uvecN — вектор из n элементов типа uint (т. е. unsigned int);
  • dvecN — вектор из n элементов типа double.



где N - это размер вектора. N может принимать значения от 2 до 4.


В наименовании типов, не только векторных, но и остальных, работает следующее правило:



  • если нет ведущей буквы b, i, u, d, обозначающей тип, то подразумевается тип float;
  • b - это значит что используется булевый тип (bool);
  • i - это тип целого числа (int);
  • u - это тип беззнакового целого (uint);
  • d - это тип с плавающей точкой двойной точности (double).



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


Для доступа к элементам вектора (от 1-го до 4-го) используются имена .x, .y, .z, .w:



vec4 myVector;

...

myVector.x=0.98;

...

float multiplyFactor=myVector.w;



Так же допустим синтаксис с буквами rgba - для работы с цветом, и stpq - для работы с текстурными координатами. Другими словами, буквы x, r, s служат для доступа к первому элементу ... а буквы w, a, q дают доступ к четвертому элементу.


Кроме того, из вектора большей размерности можно формировать вектор такой же или меньшей размерности. Например, если из вектора размерности 4 надо получить вектор размерности 2, отбросив элементы 3 и 4, то можно писать так:



vec4 myVector4;

...

vec2 myVector2=myVector4.xy



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


vec2 someVec;

vec4 differentVec = someVec.xyxx;

vec3 anotherVec = differentVec.zyw;

vec4 otherVec = someVec.xxxx + anotherVec.yxzy;



Здесь вектор differentVec получит элементы 3 и 4 равные компоненте x вектора someVec. Следующие строки понятны по-аналогии.


Но нужно иметь в виду, что можно использовать любую комбинацию до 4-х букв для создания нового вектора только тогда, когда в исходном векторе реально присутствуют запрашиваемые компоненты; например, невозможно получить доступ к компоненте .z в векторе типа vec2, так как он содержит всего 2 компоненты — .x и .y.


Для доступа к элементам вектора, помимо буквенных компонент, можно использовать числовые индексы:



vec3 position;

...

float x=position[0];



Векторы можно передавать в качестве аргументов для конструкторов векторов, уменьшая тем самым количество требуемых аргументов:



vec2 vect = vec2(0.5, 0.7);

vec4 result = vec4(vect, 0.0, 0.0);

vec4 otherResult = vec4(result.xyz, 1.0);



Матричные типы


Матрицы - это, грубо говоря, массивы размером MxN. В GLSL есть готовые типы, чтобы представлять квадратные матрицы размером от 2 до 4:



mat2 - это матрица 2x2 с элементами типа float

mat3 - это матрица 3x3 с элементами типа float

mat4 - это матрица 4x4 с элементами типа float


dmat2 - это матрица 2x2 с элементами типа double

dmat3 - это матрица 3x3 с элементами типа double

dmat4 - это матрица 4x4 с элементами типа double



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



vec4 TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;



Пример умножения матрицы на вектор, в результате чего получается вектор:



vec4 outVec = scaleMatrix * inVec;





Массивы


Одномерные массивы в GLSL по своему поведению похожи на массивы в языке Си. Размер массива фиксирован и задается с помощью константного выражения времени компиляции:



float myValues ​​[12];



Длинну массива можно узнать через метод length():



myValues.length (); // Возвращает 12



Обращение к элементу массива происходит через индекс:



myValues[5]=10;

float a=myValues[1];



Индексы в массивах начинаются с нуля. В отличие от языка Си, никаких неявных свойств вроде "имя массива является указателем на его начальный элемент" в GLSL нет.



Добавить семлеры, структуры



Запись чисел и инициализация векторов и матриц


Числа


Чтобы в момент компиляции шейдера не возникало элементарных ошибок, необходимо помнить следующие базовые правила.


Если идет работа с переменными целых типов int и uint, то числа записываются просто как целые числа:



int a = 100;

ivec3 aVec = ivec3(100, 2000, 30000);



Если идет работа с переменными вещественного типа, то числа необходимо записывать с использованием точки:



float b = 100.0;

vec3 bVec = vec3(100.0, 2000.0, 30000.0);



Похоже, что в разных версиях шейдеров разные требования к инициализации float/double переменным. В одних версиях компилятор шейдера допускает инициализацию вещественного числа литералом без точки, в других не допускает. Для гарантированной переносимости рекомендуется инициализировать вещественные числа с обязательным использованием точки.



Векторы


Люди, привыкшие к синтаксису конструкторов в C++, могут на автомате написать такую инициализацию вектора:



vec3 c(1.5, 2.7, 3.9);



- так вот, этот синтаксис не поддерживатеся в GLSL! Инициализацию вектора при создании правильно писать так:



vec3 c = vec3(1.5, 2.7, 3.9);



Матрицы


Матрицы можно инициализировать тремя способами. Первый - поэлементная инициализация, это самый непрактичный (медленный) способ:



mat2 a;

a[0][0]=0.1;

a[0][1]=0.2;

a[1][0]=0.3;

a[1][1]=0.4;



Индексы матрицы устроены так: первое число - это номер столбца, второе число - это номер строки. Таким образом, приведенный выше код создаст матрицу:



0.1 0.3

0.2 0.4



Второй способ - это "линейная" инициализация, когда начальные значения перечисляются друг за другом в конструкторе. Вначале задаются значения первого столбца, затем второго и т.д.:



mat2 a=mat2(1.1, 2.2, 3.3, 4.4);



Еще есть способ инициализации матрицы в так называемом column-major формате. В конструктор первым аргументом передается первая колонка в виде вектора, затем вторая колонка и т.д. до размерности матрицы:



// Единичная матрица в column-major формате

const mat3 mIdentity = mat3(vec3(1, 0, 0),

vec3(0, 1, 0),

vec3(0, 0, 1));




Простые Uniform-переменные


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


Например, в шейдере можно прописать uniform-переменную pixelColor:



#version 330 core

out vec4 FragColor;

uniform vec4 pixelColor;

void main()

{

   FragColor = pixelColor;

}



А значение этой переменной можно задать в C/C++ коде, перед тем, как шейдер будет запущен на выполнение:



int pixelColorLocation = glGetUniformLocation( shaderProgram, "pixelColor" );

glUseProgram( shaderProgram );

glUniform4f( pixelColorLocation, 0.0f, 0.5f, 0.0f, 1.0f );



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


В программе Bonzomatic код на C++ заранее подготавливает несколько uniform-переменных для фрагментного шейдера. Вот как они прописываются в шейдере:



uniform float fGlobalTime; // in seconds

uniform vec2 v2Resolution; // viewport resolution (in pixels)

uniform float fFrameTime; // duration of the last frame, in seconds


uniform sampler1D texFFT; // towards 0.0 is bass / lower freq, towards 1.0 is higher / treble freq

uniform sampler1D texFFTSmoothed; // this one has longer falloff and less harsh transients

uniform sampler1D texFFTIntegrated; // this is continually increasing

uniform sampler2D texPreviousFrame; // screenshot of the previous frame



Помимо простых uniform-переменных,в GLSL существуют юниформ-блоки. Здесь они не рассматриваются, но читатель должен знать, что и такие конструкции есть в спецификации GLSL.




Входящие и выходящие переменные шейдера


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


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



in type inVar1;

in type inVar2;

out type outVar;



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


out vec4 FragColor;



Однако, в случае Bonzomatic, результирующий цвет точки передается не простой выходящей шейдерной переменной, а через переменную out_color, объявленную с помощью дополнительной layout-спецификации:



layout(location = 0) out vec4 out_color;



И кроме того, в случае Bonzomatic, у фрагментного шейдера нет входящей переменной с координатами точки. Вместо этого координату точки в коде шейдера можно получить из глобальной переменной gl_FragCoord.



Особенности записи аргументов функций


Функции в GLSL определяются так же как и в языке Си++, но есть некоторые отличия.


При описании переменных функций используется обычный Си++ синтаксис, обеспечивающий передачу по значению. Синтаксиса передачи указателя или ссылки через использование звездочки "*" и амперсанда "&" не предусмотрено. Зато есть квалификаторы, которые можно писать перед параметрами функций.


  • Если квалификатора нет, то значение параметра передается в функцию по значению. Это значит, что переменная может меняться внутри функции, но в вызывающей коде значение переданной переменной меняться не будет.
  • Если перед типом аргумента написано in, то это означает, что данная переменная предназначена только для чтения, и внури функции она меняться не может. Чем-то этот квалификатор напоминает квалификатор const в языке Си++.
  • Квалификатор out означает, что такую переменную можно использовать только на запись. То есть данной переменной можно только приравнять какое-либо значение, а вот считать из нее значение не получится. Изменение значения такой переменной внутри функции меняет переменную, которая была переданна в функцию. Грубо говоря, такая передача аргумента эквивалентна передаче переменной по ссылке, причем внутри функции запрещены операции получения значения из такой переменной. Видимо, этот модификатор сделан для возможности оптимизации выполняемого кода.
  • Квалификатор inout эквивалентен передаче переменной по ссылке, при которой можно как прочитать значение переменной, так и изменить переданное значение таким образом, что изменения становятся видны за пределами функции.


Ниже дан пример описания заголовка функции:



int newFunction(vec2 coordinate, // обычный аргумент функции, передача по значению

in vec4 aVec4, // только для чтения

out vec3 aVec3, // только на запись, меняется внешняя переменная

inout int aInt); // чтение и запись, меняется внешняя переменная



Математические функции


Здесь кратко перечислены доступные математические функции:



sin(), cos(), tan(), asin(), acos(), atan(), radians(), degrees(), pow(), exp(), log(), exp2(), log2(), sqrt(), inversesqrt(), abs(), sign(), floor(), ceil(), fract(), mod(), min(), max(), clamp(), mix(), step(), smoothstep(), length(), distance(), dot(), cross(), normalize(), faceforward(), reflect(), refract(), matrix(), matrixCompMult(), lessThan(), lessThanEqual(), greaterThan(), greaterThanEqual(), equal(), notEqual(), any(), all(), not()



Более подробная информация об их использовании дана в следующей статье: Математические функции языка шейдеров GLSL.




Функции получения значения из текстур


В шейдерах GLSL имеется возможность работать с текстурами. Текстуры должны быть заранее загружены окружением в видеокарточку.


Для полчения цвета точки из двухмерной текстуры, можно воспользоваться функцией texture2D():



vec4 texture2D(sampler2D sampler, vec2 coord)

vec4 texture2D(sampler2D sampler, vec2 coord, float bias)



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


Функция texture2D() возвращает цвет в виде вектора vec4, в формате RGBA.


В некоторых реализация GLSL, эта функция доступна только во фрагментном шейдере.


Помимо двумерных текстур существуют так же и трехмерные текстуры. У них тоже можно узавать цвет в заданной точке трехмерного пространства. Для этого используется функция:



vec4 textureCube (samplerCube sampler, vec3ordin)

vec4 textureCube (samplerCube sampler, vec3ordin, float bias)



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



Прикрепленные файлы:
Так же в этом разделе:
 
MyTetra Share v.0.58
Яндекс индекс цитирования