Введение
Предыдущие заметки использовали один шейдер и одну uniform-переменную, что позволяло обходиться интерфейсом, предоставляемым OpenGL. Данный подход сильно усложняет разработку, когда шейдеров становится два и более.
Работа с Uniform-переменными может быть весьма трудоемкой, так как требует обновления данных в случае смены активного шейдера, а так же организацию вызова функции загрузки для каждой отдельной переменной. Для решения данной проблемы может использовать Uniform-буфер.
Uniform-буфер
Uniform-буфер — это общая между шейдерами область памяти. Следовательно обновление данных при смене шейдера не требуется.
Uniform-блок — описание памяти по которому шейдер будет трактовать доступные данные.
Блок и буфер должны быть связаны через индекс из массива точек привязки (binding points), изображенный на рисунке 1.
Дополним перечисление (enum) из файла include/Buffers.h:
// Тип буфера
enum BUFFER_TYPE { VERTEX = GL_ARRAY_BUFFER
, ELEMENT = GL_ELEMENT_ARRAY_BUFFER
, UNIFORM = GL_UNIFORM_BUFFER
};
Важное замечание: для таких uniform-буферов конфигурация атрибутов не требуется (glVertexAttribPointer).
Связь uniform-блока с индексом точки привязки осуществляется путем единоразового вызова для каждой шейдерной программы (после компиляции шейдера) функции glUniformBlockBinding, которая принимает три аргумента:
- program — дескриптор шейдерной программы;
- uniformBlockIndex — дескриптор uniform-блока;
- uniformBlockBinding — индекс блока привязки.
Для получения дескриптора uniform-блока используется функция glGetUniformBlockIndex, которая принимает следующие аргументы:
- program — дескриптор шейдерной программы;
- uniformBlockName — строка с именем uniform-блока.
Начиная с версии OpenGL 4.2 появилась возможность указать в шейдере точку привязки, что избавляет от необходимости вызывать две предыдущие функции. Пример указания точки привязки в шейдере:
layout(binding = 2) uniform Lights { ... };
Дополнительно можно указать в скобках параметр std140, который указывает драйверу правила, в соответствии с которыми данные будут размещаться внутри буфера. При отсутствии этого параметра решение о расположении переменных принимает драйвер.
Важное замечание: в uniform-блоках используется выравнивание по 16 байт, следовательно использовать спецификатор alignas(16) перед типом переменной.
Пример выравнивания:
struct Light
{
alignas(16) glm::vec3 position;
alignas(16) glm::vec3 color;
float power;
};
Из примера можно сделать вывод, что последнее поле можно не выравнивать для экономии памяти.
Связь uniform-буфера с индексом точки привязки осуществляется путем единоразового вызова (после первой загрузки данных в буфер) функции glBindBufferBase, которая принимает в качестве аргументов:
- target — тип буфера;
- index — индекс блока привязки;
- buffer — дескриптор созданного uniform-буфера.
С помощью функции glBindBufferRange можно привязать часть буфера, которая задается двумя дополнительными аргументами:
- offset — отступ в байтах;
- size — размер в байтах.
Данные в uniform-буфер можно загрузить с помощью функции glBufferData, которая рассматривалась во второй заметке.
Примечание: можно создать пустой буфер заданного размера для последующей привязки, передав в качестве указателя на загружаемые данные NULL.
Функция glBufferSubData позволяет записать в буфер лишь часть данных, определяемую вторым (offset) и третьим (size) параметрами.
Для удобства работы и последующей привязки создадим класс UBO, наследник от BO:
// Объект uniform-буфера
class UBO : public BO
{
public:
UBO(int size, int binding); // Создает пустой uniform-буфер заданного размера с автоматической привязкой
UBO(const void *data, int size, int binding); // Создает пустой uniform-буфер заданного размера с автоматической привязкой
void rebind(int binding); // Перепривязка
void loadSub(const void *data, int size, int offset = 0); // Загрузка с отступом};
Важное замечание: необходимо поменять область видимости у дескриптора и типа буфера в классе BO на protected, чтобы метод UBO::rebind мог их использовать.
Реализация конструкторов и метода перепривязки:
// Создает пустой uniform-буфер заданного размера с автоматической привязкой
UBO::UBO(int size, int binding) : BO(UNIFORM, 0, size)
{
rebind(binding);
}
// Создает пустой uniform-буфер заданного размера с автоматической привязкой
UBO::UBO(const void *data, int size, int binding) : BO(UNIFORM, data, size)
{
rebind(binding);
}
// Перепривязка
void UBO::rebind(int binding)
{
glBindBufferBase(type, binding, handler);
}
// Загрузка с отступом
void UBO::loadSub(const void *data, int size, int offset)
{
use();
glBufferSubData(type, offset, size, data);
}
Важное замечание: необходимо добавить вызов метода BO::use внутри метода BO::load, иначе он не будет корректно работать загрузка для uniform-буфера.
Ниже приводятся примеры использования нового класса в совокупности с шейдером для раздельной передачи матриц камеры и объекта.
В файле src/main.cpp изменим uniform-переменную и добавим наш класс:
// Расположение Uniform-переменной
GLuint vp_uniform = glGetUniformLocation(shaderProgram, "vp");
GLuint model_uniform = glGetUniformLocation(shaderProgram, "model");
// Uniform-буферы.
UBO cameraUB(sizeof(glm::mat4)*2, 0);
В цикле отрисовки добавим загрузку данных о камере и поменяем переменную, передаваемую в метод GrouptedModel::render:
...
cameraUB.loadSub(&Camera::current().getProjection(), sizeof(glm::mat4), 0);
cameraUB.loadSub(&Camera::current().getView(), sizeof(glm::mat4), sizeof(glm::mat4));
...
scene.render(model_uniform);
...
В файле shaders/shader.vert:
...
layout(std140, binding = 0) uniform Camera
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
...
void main()
{
gl_Position = projection * view * model * vec4(pos, 1.0);
...
Данные, хранимые в буфере можно использовать как в вершинном, так и в фрагментном шейдере, для этого нужно при объявлении задать разные имена для объектов uniform-блока. Пример:
В вершинном шейдере:
layout(binding = 2) uniform Lights { ... } lights_v;
В фрагментном шейдере:
layout(binding = 2) uniform Lights { ... } lights_f;
Либо не объявлять имя объекта uniform-блока, тогда поля используются как простые глобальные переменные (без необходимости использовать имя объекта перед именем поля):
В вершинном шейдере:
layout(binding = 2) uniform Lights { ... };
В фрагментном шейдере:
layout(binding = 2) uniform Lights { ... };
Текущая версия доступна на теге v0.1 в репозитории 06.
Класс шейдерной программы
С увеличением числа шейдеров работа с переменными становится неудобной.
Создадим класс шейдера в файле include/Shader.h:
// Класс шейдерной программы
class ShaderProgram
{
public:
ShaderProgram();
ShaderProgram(const ShaderProgram ©);
~ShaderProgram();
ShaderProgram& operator=(const ShaderProgram& other);
void use(); // Использование шейдеров
void load(GLuint type, const char* filename); // Функция для загрузки шейдеров
void link(); // Формирование программы из загруженных шейдеров
GLuint getUniformLoc(const char* name); // Возвращает местоположение uniform-переменной
GLuint bindUniformBlock(const char* name, int binding); // Привязка uniform-блока
void bindTextures(const char* textures_base_shader_names[], int count); // Инициализация текстур на шейдере
private:
GLuint program; // Дескриптор
static std::map<int, int> handler_count; // Получение количества использований по дескриптору шейдера (Shared pointer)
std::map<const char*, GLuint> uniformLocations; // Местоположения uniform-переменных
};
Метод ShaderProgram::load — видоизмененная версия функции из файла src/main.cpp, которую можно удалить:
// Функция чтения шейдера из файла
std::string readFile(const char* filename)
{
std::string text;
std::ifstream file(filename, std::ios::in); // Открываем файл на чтение
// Если файл доступен и успешно открыт
if (file.is_open())
{
std::stringstream sstr; // Буфер для чтения
sstr << file.rdbuf(); // Считываем файл
text = sstr.str(); // Преобразуем буфер к строке
file.close(); // Закрываем файл
}
return text;
}
// Функция для загрузки шейдеров
void ShaderProgram::load(GLuint type, const char* filename)
{
// Создание дескрипторов шейдера
GLuint handler = glCreateShader(type);
// Переменные под результат компиляции
GLint result = GL_FALSE;
int infoLogLength;
// Считываем текст вершинного шейдера
std::string code = readFile(filename);
const char* pointer = code.c_str(); // Преобразование к указателю на const char, так как функция принимает массив си-строк
// Компиляция кода вершинного шейдера
glShaderSource(handler, 1, &pointer, NULL);
glCompileShader(handler);
// Проверка результата компиляции
glGetShaderiv(handler, GL_COMPILE_STATUS, &result);
glGetShaderiv(handler, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0)
{
char* errorMessage = new char[infoLogLength + 1];
glGetShaderInfoLog(handler, infoLogLength, NULL, errorMessage);
std::cout << errorMessage;
delete errorMessage;
}
// Привязка скомпилированного шейдера
glAttachShader(program, handler);
// Освобождение дескриптора шейдера
glDeleteShader(handler);
}
// Формирование программы из загруженных шейдеров
void ShaderProgram::link()
{
// Переменные под результат компиляции
GLint result = GL_FALSE;
int infoLogLength;
// Формирование программы из привязанных шейдеров
glLinkProgram(program);
// Проверка программы
glGetProgramiv(program, GL_LINK_STATUS, &result);
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0)
{
char* errorMessage = new char[infoLogLength + 1];
glGetProgramInfoLog(program, infoLogLength, NULL, errorMessage);
std::cout << errorMessage;
delete errorMessage;
}
// Используем шейдерную программу объекта из которого вызван метод
this->use();
}
Словарь (map) используется для оптимизации вызовов функции glGetUniformLocation, вызываемой через метод ShaderProgram::getUniformLoc:
// Возвращает местоположение uniform-переменной
GLuint ShaderProgram::getUniformLoc(const char* name)
{
GLuint result; // Результат
// Если такую переменную ещё не искали - найдем, иначе вернем уже известный дескриптор
if (!uniformLocations.count(name))
uniformLocations[name] = result = glGetUniformLocation(program, name);
else
result = uniformLocations[name];
return result;
}
Аналогично классу текстуры — ведется подсчет экземпляров класса и удаление шейдерной программы в случае, если ей ни один объект не пользуется:
std::map<int, int> ShaderProgram::handler_count; // Получение количества использований по дескриптору ШП (Shared pointer)
ShaderProgram::ShaderProgram()
{
program = glCreateProgram();
handler_count[program] = 1;
}
ShaderProgram::ShaderProgram(const ShaderProgram ©) : program(copy.program)
{
handler_count[program]++;
}
ShaderProgram::~ShaderProgram()
{
if (!--handler_count[program]) // Если количество ссылок = 0
{
// Удаление шейдерной программы
glDeleteProgram(program);
}
}
// Оператор присваивания
ShaderProgram& ShaderProgram::operator=(const ShaderProgram& other)
{
// Если это разные шейдерные программы
if (program != other.program)
{
this->~ShaderProgram(); // Уничтожаем имеющуюся
// Заменяем новой
program = other.program;
handler_count[program]++;
}
return *this;
}
Метод ShaderProgram::use является оберткой для функции glUseProgram и устанавливает шейдерную программу как активную:
void ShaderProgram::use()
{
glUseProgram(program);
}
Для привязки uniform-блока используется метод ShaderProgram::bindUniformBlock:
void ShaderProgram::bindUniformBlock(const char* name, int binding)
{
glUniformBlockBinding( program
, glGetUniformBlockIndex(program, name)
, binding);
}
Для упрощения работы с текстурами необходимо перенести метод Texture::init_textures в класс ShaderProgram::bindTextures:
// Инициализация текстур на шейдере
void ShaderProgram::bindTextures(const char* textures_base_shader_names[], int count)
{
// Цикл по всем доступным текстурам
for (int i = 0; i < count; i++)
glUniform1i(getUniformLoc(textures_base_shader_names[i]), i);
}
Пример использования класса в файле src/main.cpp:
// Базовый шейдер
ShaderProgram base;
// Загрузка и компиляция шейдеров
base.load(GL_VERTEX_SHADER, "shaders/shader.vert");
base.load(GL_FRAGMENT_SHADER, "shaders/shader.frag");
base.link();
// Установим значения текстур
const char* textures_base_shader_names[] = {"tex_diffuse"};
base.bindTextures(textures_base_shader_names, sizeof(textures_base_shader_names)/sizeof(const char*));
Старую функцию удаления шейдера из конца функции main необходимо убрать.
Текущая версия доступна на теге v0.2 в репозитории 06.
Заключение
В ходе заметки был рассмотрен новый вид uniform-данных — uniform-буфер и разработан класс для удобной работы с ним.
Помимо этого был разработан класс для работы с шейдерными программами и произведена оптимизация функции взятия местоположения uniform-переменных.
Проект доступен в публичном репозитории: 06
Библиотеки: dependencies
5 ответов к “OpenGL 6: класс шейдерной программы”
Доброго времени!
Не могли бы помочь с шейдерной программой шума Перлина
Мне надо из Java перевести в Visual c++ 2019 Opengl
Напишите мне на почту ваш телеграмм
Я сам математик. Из Алтайского края. Барнаул.
Появление у вызывающей стороны нескомпилированного объекта шейдера — потенциальный выстрел себе в ногу, потому что нет гарантий, что его инициализируют перед использованием. Можно добавить специальный класс, который возьмёт логику создания шейдера на себя, а у шейдера конструкторы скрыть
Вы правы. Можете добавить исключение или assert в конструктор шейдера.
Я дополню статью чуть позже.