Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Стандартные методы задания значений для uniform-переменных в шейдере (через вызов glUniform*) при своей простоте хорошо работаю для небольшого количества таких переменных. Однако когда нужно часто обновлять значения большого количества uniform-переменных, такой подход оказывается явно неподходящим.
Существует несколько способов организации работы с uniform-переменными в таких случаях. Одним из таких методов является организация таких переменных в массивы и использования расширения EXT_bindable_uniform. Это позволяет привязать к такому массиву VBO и задавать данные через него. Однако организация данных в виде однородных массив не всегда удобна.
Расширение ARB_uniform_buffer_object (вошедшее в состав OpenGL 3.1) предлагает и другой вариант. Это расширение позволяет организовывать uniform-переменные в так называемые uniform-блоки, при этом в качестве источника данных для этих блоков выступают VBO с типов GL_UNIFORM_BUFFER.
Группу uniform-переменных (за исключением переменных-сэмплеров) в шейдере можно объединить в именованный uniform-блок. Пример такого блока приводится ниже.
uniform MyBlock { mat4 m; vec3 v; bool flag; uvec4 mask; };
При этом сам шейдер позволяет обращаться к переменным по их именам без указания имени блока, как если бы они были обычными uniform-переменными. Тем самым если в работающем шейдере мы сгруппировали переменные в uniform-блок, то ничего в этом шейдере менять не надо - изменится только способ, каким приложение задает значения этих переменных. Все uniform-переменные, не входящие ни в один именованный uniform-блок, считаются членами неименованного (default) uniform-блока.
В описании блока при помощи директивы layout можно явно задать один из трех способов для управления размещением переменных внутри соответствующего буфера. Поддерживаются следующие способы - packed, shared и std140.
layout (std140) uniform MyBlock { mat4 m; vec3 v; bool flag; uvec4 mask; };
Формат packed эффективно размещает переменные в памяти, при этом способ размещения зависит от реализации. Кроме того, неиспользуемые переменные могут быть отброшены.
Формат shared (этот формат используется по умолчанию, т.е. когда формат явно не указан) также использует зависящую от реализации раскладку переменных в памяти. Однако этот формат однозначно определяется описанием блока, поэтому если есть несколько разных шейдеров, использующих у себя один и тот же uniform-блок, то его формат будет одинаков для всех использующих его шейдеров. Это обстоятельство позволяет использовать один и тот-же буфер с данными сразу для нескольких шейдеров.
Последний формат std140 идет еще дальше - он является полностью переносимым и не зависит от реализации. Для него явно заданы правила размещения переменных в памяти. При этом с точки зрения объема требуемой памяти он может быть не самым оптимальным, но для него гарантируется одинаковый формат буфера не только для разных шейдеров, но и для разных производителей GPU и драйверов.
Переменные типа bool и uint хранятся как значения типа GLuint, при этом ненулевое значение соответствует true. Типы float и int хранятся как соответствующие значения для CPU. Вектора из N элементов хранятся как N последовательно идущих значений соответствующего типа. Таким образом bvec3 будет храниться как три значения типа GLuint.
Матрица CxR column-major хранится как массив из C векторов-столбцов из R элементов типа float. При этом расстояние в байтах между соседними векторами может не совпадать с размером вектора и обозначается как matrix stride.
Аналогично row-major матрица CxR хранится как массив из R векторов-строк из C элементов. Массив из скаляров, векторов, матриц хранится последовательно, начиная с нулевого элемента. При этом расстояние между подряд идущими элементами может не совпадать с размером элемента и называется array stride.
Расположение в памяти переменных uniform-блока подчиняются довольно простым правилам. Все переменные располагаются последовательно именно в том порядке, в котором они перечислены в блоке. Каждая переменная из блока (включая структуры и их члены) характеризуются смещением внутри блока (offset) и выравниванием (alignment). Правила задаются рекурсивно в терминах структуры и ее полей. Весь uniform-блок рассматривается как одна структура, ее смещение равно нулю. Смещение первого члена структуры совпадает со смещением самой структуры. Смещение для следующего элемента определяется как смещение последнего байта предыдущего элемента, округленное вверх в соответствии с его выравниванием.
Ниже приводятся 10 правил, полностью определяющих расположение переменных в памяти для формата std140.
Рассмотрим выравнивание и расположение в памяти ряда переменных:
На следующем рисунке приводится размещение в памяти ряда простейших типов, один блок соответствует 4 байтам, перечеркнутый блок данных не хранит и используется для выравнивания.
Рис 1. Примеры размещения в памяти переменных разных типов.
Основным преимуществом формата блока std140 является его полная прозрачность - можно заранее рассчитать положения для всех переменных и использовать их для доступа напрямую в вершинных буфер. Для других форматов необходимо для каждой переменной отдельно получать информацию об ее смещении в блоке.
Переменные, размещенные внутри именованного блока, принципиально отличаются от обычных uniform-переменных (размещенных в неименованном блоке). Если для установки значения переменным из неименованного блока используются функции glUniform* и положение (location) переменной (получаемое по ее имени), то для переменных из именованного uniform-блока такой способ не работает. Значения задаются сразу для всех переменных блока через соответствующий буфер.
Рассмотрим каким именно способом осуществляется задание значения для этих переменных. Прежде всего необходимо по имени uniform-блока получить его индекс при помощи функции glGetUniformBlockIndex.
GLuint glGetUniformBlockIndex ( GLuint program, const char * blockName );
Для получения информации по uniform-блоку по его индексу служит функция glGetActiveUniformBlockiv:
void glGetActiveUniformBlockiv ( GLuint program, GLuint blockIndex, GLenum pname, int * params );
Параметры program и blockIndex задают программу и uniform-блок из этой программы, параметр params указывает куда должны быть записаны результаты запроса, а параметр pname задает какую именно информацию необходимо вернуть (см. таблицу 1).
Таблица 1. Допустимые значение параметра pname для функции glGetActiveUniformBlockiv.
Название | Комментарий |
---|---|
GL_UNIFORM_BLOCK_BINDING | Возвращает точку привязки VBO к uniform-буферу |
GL_UNIFORM_BLOCK_DATA_SIZE | Возвращает размер блока в единицах соответствующего типа |
GL_UNIFORM_BLOCK_NAME_LENGTH | Возвращает длину имени блока с учетом '\0' |
GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS | Возвращает число активных uniform-переменных в блоке |
GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES | Возвращает массив индексов активных переменных блока |
GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER | Возвращается ненулевое значение, если данный блок используется вершинным шейдером |
GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER | Возвращается ненулевое значение, если данный блок используется фрагментным шейдером |
GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER | Возвращается ненулевое значение, если данный блок используется геометрическим шейдером |
Следующий пример демонстрирует получение индекса блока и его размера.
GLuint blockIndex = glGetUniformBlockIndex ( program, "MyBlock" ); int size; int numUniforms; glGetActiveUniformBlockiv ( program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &size ); glGetActiveUniformBlockiv ( program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &numUniforms );
Каждая uniform-переменная при линковке получает свой уникальный индекс, индексы начинаются с нуля и идут последовательно. Для того, чтобы получить массив индексов по массив имен переменных служит функция glGetUniformIndices
void glGetUniformIndices ( GLuint program, GLsizei count, const char ** uniformNames, GLuint * uniformIndices );
Данная функция позволяет получить информацию по целой группе переменных, причем возможно даже из разных uniform-блоков. Параметр count задает количество переменных, для которых необходимо вернуть индексы, в массиве uniformNames содержится count имен переменных, а массив uniformIndices должен содержать достаточно места для размещения полученных индексов. В результате вызова в массив uniformIndices будут записаны индексы для заданных имен переменных, GL_INVALID_INDEX записывается в том случае, когда переданное имя не является именем переменной из какого-либо uniform-блока.
Для получения полной информации о группе переменных из uniform-блока по массиву индексов используется функция glGetActiveUniformsiv.
void glGetActiveUniformsiv ( GLuint program, GLsizei uniformCount, const GLuint * uniformIndices, GLenum pname, int * params );
В следующей таблице приводятся допустимые значения параметра pname и что будет возвращено для каждого допустимого значения.
Таблица 2. Допустимые значение параметра pname для функции glGetActiveUniformsiv.
Название | Комментарий |
---|---|
GL_UNIFORM_TYPE | Возвращает тип переменной. Допустимые значения - GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2_EXT, GL_UNSIGNED_INT_VEC3_EXT, GL_UNSIGNED_INT_VEC4_EXT, GL_BOOL, GL_BOOL_VEC2, GL_BOOL_VEC3, GL_BOOL_VEC4, GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3x4, GL_FLOAT_MAT4x2 и GL_FLOAT_MAT4x3 |
GL_UNIFORM_SIZE | Возвращает размер соответствующей переменной в байтах |
GL_UNIFORM_NAME_LENGTH | Возвращает длину имени переменной с учетом '\0' |
GL_UNIFORM_BLOCK_INDEX | Возвращает индекс uniform-блока, содержащего данную переменную |
GL_UNIFORM_OFFSET | Возвращает смещение переменной от начала блока |
GL_UNIFORM_ARRAY_STRIDE | Возвращается array stride, если переменная является массивом |
GL_UNIFORM_MATRIX_STRIDE | Возвращается matrix stride, если переменная является матрицей |
GL_UNIFORM_IS_ROW_MAJOR | Возвращается ненулевое значение, если переменная является row-major матрицей |
В следующем примере получается необходимая информация о переменных uniform-блока.
const char * names [NUM] = { "diffColor", "specColor", "specPower", "emission" }; GLuint index [NUM]; // index for every variable int offset [NUM], size [NUM]; glGetUniformIndices ( program, NUM, names, index ); glGetActiveUniformsiv ( program, NUM, index, GL_UNIFORM_OFFSET, offset ); glGetActiveUniformsiv ( program, NUM, index, GL_UNIFORM_SIZE, size );
Значения для переменных uniform-блока берутся из VBO с типом GL_UNIFORM_BUFFER (UBO). В отличии от "привязки" (bind) для обычных буферов, когда буфер привязывается к конкретному типу, для UBO (также как и для буферов, используемых при transform feedback) используется так называемая индексированная привязка - сразу к типу (target) и целочисленному индексу.
Привязка к индексу (target всегда равен GL_UNIFORM_BUFFER) позволяет в одной программе иметь сразу несколько uniform-блоков и для каждого из них назначить свой буфер, снабжающий его данными. Для привязки буфера по индексу служат следующие функции.
void glBindBufferRange ( GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeptr size ); void glBindBufferBase ( GLenum target, GLuint index, GLuint buffer );
Параметр target принимает значение GL_UNIFORM_BUFFER, параметр buffer задает используемый VBO, а параметр indexзадает точку привязки буфера. По этой точке привязки осуществляется связывание uniform-блока и буфера. Для этого используется функция glUniformBlockBinding.
void glUniformBlockBinding ( GLuint program, GLuint uniformBlockIndex, GLuint bindingIndex );
Здесь параметр bindingIndex задает точку привязки (из команды glBindBufferBase/glBindBufferRange) вершинного буфера к unifrom-блоку uniformBlockIndex.
Рассмотрим простейший пример использования UBO. Ниже приводятся шейдеры (мне очень понравился подход, помещающий все шейдеры в один файл и разделяя их строкой вида "-- тип-шейдера"), использующие uniform-блок.
-- vertex
#version 330 core
uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye; // eye position
uniform vec3 light;
in vec3 pos; // position in xyz
in vec3 normal;
out vec3 n;
out vec3 v;
out vec3 l;
out vec3 h;
void main(void)
{
vec4 p = mv * vec4 ( pos, 1.0 );
gl_Position = proj * p;
n = normalize ( nm * normal );
v = normalize ( eye - p.xyz ); // vector to the eye
l = normalize ( light - p.xyz );
h = normalize ( l + v );
}
-- fragment
#version 330 core
uniform Lighting
{
vec4 diffColor;
vec4 specColor;
float specPower;
};
in vec3 n;
in vec3 v;
in vec3 l;
in vec3 h;
out vec4 color;
void main(void)
{
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
vec3 h2 = normalize ( h );
vec4 diff = diffColor * max ( dot ( n2, l2 ), 0.1 );
vec4 spec = specColor * pow ( max ( dot ( n2, h2 ), 0.0 ), specPower );
color = diff + spec;
}
Полностью исходный код можно скачать, ниже приводится только фрагмент - функция, отвечающая за создание и обновление UBO и данных. Обратите внимание, что вся инициализация делается всего один раз. При этом инициализируется буфер и получаются смещения для используемых переменных внутри uniform-блока. После этого достаточно только отобразить буфер в память приложения и произвести запись по ранее полученным смещениям. При этом подобный подход не зависит от используемого формата буфера.
void setupUbo ( VertexBuffer& buf, int bindingPoint, const vec4 dColor, const vec4 sColor, float specPwr )
{
static const char * names [3] =
{
"diffColor",
"specColor",
"specPower"
};
static GLuint index [3]; // index for every variable
static int offset [3];
static GLuint blockIndex;
static int blockSize;
static bool inited = false;
if ( !inited )
{
inited = true;
blockIndex = program.indexForUniformBlock ( "Lighting" );
blockSize = program.uniformBlockSize ( blockIndex );
glGetUniformIndices ( program.getProgram (), 3, names, index );
glGetActiveUniformsiv ( program.getProgram (), 3, index, GL_UNIFORM_OFFSET, offset );
// init with zero's
byte * buffer = new byte [blockSize];
memset ( buffer, 0, blockSize );
buf.create ();
buf.bindBase ( GL_UNIFORM_BUFFER, bindingPoint );
buf.setData ( blockSize, buffer, GL_STREAM_DRAW );
delete buffer;
}
buf.bindBase ( GL_UNIFORM_BUFFER, bindingPoint );
program.bindBufferToIndex ( blockIndex, bindingPoint );
byte * ptr = (byte *) buf.map ( GL_WRITE_ONLY );
memcpy ( ptr + offset [0], &dColor.x, 16 );
memcpy ( ptr + offset [1], &sColor.x, 16 );
memcpy ( ptr + offset [2], &specPwr, 4 );
buf.unmap ();
}
По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.