GLSL

Xtreme3D

Урок 19
Основы GLSL

Уровень: опытный
Версия Xtreme3D: 3.0.x
Автор урока: Gecko

GLSL (OpenGL Shading Language) - это высокоуровневый язык описания шейдеров. С его помощью вы можете запрограммировать графический конвейер - иными словами, управлять рендерингом объектов на вершинном и пиксельном уровне. За обработку вершин отвечает вершинная программа GLSL, за обработку пикселей - фрагментная.
Работа с GLSL подразумевает знание принципов растеризации и графического конвейера OpenGL, а также линейной алгебры. Поскольку сам Xtreme3D не требует этих знаний, использование GLSL может быть весьма трудной задачей для начинающего, поэтому рекомендуем предварительно почитать книги или руководства по данной теме. Очень полезным будет знакомство с принципами работы в OpenGL, а также хотя бы базовое знание C/C++.

Типы данных GLSL



GLSL является строго типизированным языком - любая переменная в нем имеет определенный тип. Язык поддерживает следующие основные типы:

bool - логическое значение
int - целое число со знаком
uint - беззнаковое целое число
float - число с плавающей запятой одинарной точности
double - число с плавающей запятой двойной точности
bvec2, bvec3, bvec4 - вектор логических значений (размерности 2, 3 и 4)
ivec2, ivec3, ivec4 - вектор целых чисел
uvec2, uvec3, uvec4 - вектор беззнаковых целых чисел
vec2, vec3, vec4 - вектор чисел с плавающей запятой
dvecn2, dvecn3, dvecn4 - вектор чисел с плавающей запятой двойной точности
mat2, mat3, mat4 - матрица 2x2, 3x3, 4x4
sampler2D - текстура
sampler2DCube - кубическая текстура
sampler2DShadow - теневая текстура
void - ключевое слово, обозначающее отсутствие типа (для функций без возвращаемого результата).

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


Вершинная программа принимает координаты вершин и их атрибуты (такие как нормали и тангенты) и, как правило, переводит их из объектного пространства в пространство отсечения, в мировое или в видовое пространство.

- Объектное пространство (object space) - это локальное пространство объекта. Центром координатной системы в нем является центр объекта - вершины модели заданы относительно этого центра.
- Мировое пространство (world space) - другое название для абсолютного пространства. Центром координатной системы в нем является точка (0, 0, 0). Совокупная трансформация объекта (перенос, поворот и масштабирование) переводит вершины из локального в мировое пространство. Эта трансформация обычно хранится и передается в шейдер в виде матрицы 4x4 - так называемой модельной матрицы (model matrix).
- Видовое пространство (eye space) - пространство, в котором центром координатной системы является позиция камеры. Перевод вершин из мирового в видовое пространство осуществляется при помощи обратной матрицы преобразования камеры - так называемой видовой матрицы (view matrix). В OpenGL, как правило, модельная матрица и видовая совмещены в одну - модельно-видовую (modelview matrix).
- Пространство отсечения (clip space) - пространство, в которое вершины переводятся матрицей проекции (projection matrix).
Необходимо отметить, что вершины в GLSL хранятся в так называемых однородных координатах (homogeneous coordinates) - то есть, имеют дополнительную четвертую координату W. Такими координатами можно выражать бесконечно удаленные точки, когда W равна нулю. Обычные точки имеют W равную 1.
Вершины в пространстве отсечения являются главным результатом работы вершинного шейдера. Простейший вершинный шейдер, выполняющий только перевод вершин из объектного пространства в пространство отсечения, выглядит следующим образом:

void main()
{
  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

gl_Vertex - входные координаты вершины
gl_Position - выходные координаты вершины
gl_ModelViewProjectionMatrix - встроенная матрица 4х4, комбинация модельно-видовой и проекционной матриц OpenGL.
Для данной операции, кстати, в GLSL имеется встроенная функция ftransform:

gl_Position = ftransform();

Вершинному шейдеру также доступны другие атрибуты вершины - нормаль, цвет и текстурные координаты: gl_Normal, gl_Color, gl_MultiTexCoordN (где N - номер от 0 до 7). Обычно эти атрибуты интерполируются между тремя вершинами треугольника, а затем поступают во фрагментный шейдер. Чтобы передать какое-либо значение на интерполяцию, используются промежуточные varying-переменные. Например, так выглядит шейдер, передающий на интерполяцию нормали:

varying vec3 normal;

void main()
{
  normal = gl_NormalMatrix * gl_Normal;
  gl_Position = ftransform();
}

Обратите внимание, что мы переводим нормаль вершины из объектного пространства в видовое при помощи специальной встроенной матрицы 3x3 gl_NormalMatrix. Это необходимо для того, чтобы оптимальным образом рассчитывать освещение в пиксельном шейдере - это делается именно в видовом пространстве: тот факт, что камера находится в точке (0,0,0), значительно облегчает вычисления, связанные с бликовой компонентой освещенности.
С передачей текстурных координат шейдер будет выглядеть так:

varying vec3 normal;

void main()
{
  normal = gl_NormalMatrix * gl_Normal;
  gl_TexCoord[0] = gl_MultiTexCoord0;
  gl_Position = ftransform();
}

gl_TexCoord - это встроенная varying-переменная, массив, через который вы можете передавать любые данные, не только текстурные координаты.

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


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

void main()
{
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

gl_FragColor - выходной цвет пикселя.
В данном случае vec4(1.0, 0.0, 0.0, 1.0) обозначает красный цвет с прозрачностью 1.0 (полная непрозрачность).

Использование шейдеров в Xtreme3D


Создавать шейдеры GLSL и подключать их к материалам очень просто:

vp = TextRead('my_vertex_shader.glsl');
fp = TextRead('my_fragment_shader.glsl');
shader = GLSLShaderCreate(vp, fp);
MaterialSetShader('myMaterial', shader);

Освещение на GLSL


Чтобы реализовать простейшее освещение по формуле Ламберта, нам нужны координаты точки поверхности, нормаль в этой точке, а также координаты источника света. Таким образом, нам понадобятся, по меньшей мере, две varying-переменные - нормаль и интерполированные координаты вершины.
Вершинный шейдер:

varying vec3 normal;
varying vec3 position;

void main()
{
  normal = gl_NormalMatrix * gl_Normal;
  position = (gl_ModelViewMatrix * gl_Vertex).xyz;
  gl_Position = ftransform();
}
gl_ModelViewMatrix - это встроенная матрица 4x4, модельно-видовая матрица OpenGL. Она переводит координаты из объектного пространства в видовое, в котором мы будем вычислять освещение. Поскольку результат этого перевода - однородный вектор vec4, мы отбрасываем координату W и берем только вектор XYZ.
Фрагментный шейдер:

varying vec3 normal;
varying vec3 position;

void main()
{
  vec3 N = normalize(normal);
  vec3 L = normalize(gl_LightSource[0].position.xyz - position);
  float diffuse = clamp(dot(N, L), 0.0, 1.0);
  vec4 color = gl_FrontMaterial.diffuse * diffuse;
  color.a = 1.0;
  gl_FragColor = color;
}

Обратите внимание, что при передаче во фрагментый щейдер единичные векторы (такие, как нормаль) после интерполяции нужно обязательно нормировать - видеокарта не делает это за вас. Для этого в GLSL есть функция normalize.
Доступ к координатам источника света осуществляется при помощи атрибута position встроенного объекта gl_LightSource (массив из 8 элементов, по числу источников света OpenGL). Эти координаты во фрагментном шейдере уже автоматически переведены в видовое пространство, что очень удобно - не нужно делать это вручную. Но если вам, по тем или иным причинам, нужно вычислять освещение в другом пространстве - например, в пространстве касательных - то не забудьте соответствующим образом трансформировать их. Эти координаты также однородные: точечный источник света, как правило, имеет координату W равную 1, направленный - равную 0.
Операция dot(N, L) - это и есть расчет освещенности по формуле Ламберта: освещенность в точке определяется плотностью света, а она линейно зависит от косинуса угла падения света. Косинус угла между двумя единичными векторами равен их скалярному произведению (dot product).
Поскольку результат этой операции - скаляр (float), для передачи в gl_FragColor нужно помножить это значение на какой-нибудь цвет. Лучше всего использовать диффузный цвет материала - gl_FrontMaterial.diffuse: таким образом, вы можете контролировать цвет объекта вне шейдера, функцией MaterialSetDiffuseColor.

Текстуры


Во фрагментном шейдере можно читать цвет из текстур - для этого используется функция texture2D:

uniform sampler2D diffuseTexture;

void main()
{
  vec4 texColor = texture2D(diffuseTexture, gl_TexCoord[0].xy);
  gl_FragColor = texColor;
}

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

param = GLSLShaderCreateParameter(shader, 'diffuseTexture');
GLSLShaderSetParameterTexture(param, 'myMaterial', 0);


В функцию GLSLShaderCreateParameter передается имя uniform-объекта. В функцию GLSLShaderSetParameterTexture передается имя материала, из которого нужно прочитать текстуру, а также текстурный блок, через который нужно передавать текстуру. Стандарт OpenGL гаранирует 8 доступных текстурных блоков (0-7) - у современных видеокарт их может быть и больше (до 16 и даже 32), но для лучшей совместимости рекомендуется не использовать больше 8. В одном шейдере нельзя передавать две разные текстуры через один и тот же текстурный блок - то есть, если вы передаете несколько текстур в разные uniform-параметры, используйте для них разные блоки.

О версиях GLSL


Xtreme3D базируется на OpenGL 1.x и некоторых функциях из OpenGL 2.х, которые подключаются через расширения ARB. Таким образом, движок поддерживает GLSL версий 1.1 и 1.2 - более поздние версии языка определены уже в рамках спецификации OpenGL 3.0.
По умолчанию используется GLSL 1.1. Чтобы переключиться на 1.2, используйте директиву препроцессора (на первой строке шейдера):

#version 120

Версия 1.2 отличается встроенной поддержкой транспонирования матриц (функция transpose), неквадратных матриц, а также массивов.