Все статьи Скручивание
Для установки параметров шейдера приложение может обращаться к uniform-переменным шейдера. Мы построим шейдер, выполняющий «закручивание» вершин вокруг оси Y.
Приложения могут объявлять в шейдерах собственные uniform-переменные и устанавливать их значения. Uniform-переменные будут использоваться шейдерной программой в режиме «только для чтения» для каждой вершины и каждого фрагмента. Мы построим пример шейдера, выполняющего «закручивание» вершин произвольного объекта вокруг оси Y. Действие шейдера мы оценим на примере специального тела — Зонтика Уитни:
Класс CWhitneyUmbrella
Для этой поверхности нам не нужны текстурные координаты, поэтому мы используем следующее объявление вершины:
// Вершина с трёхмерной позицией и нормалью.
struct SVertexP3N
{
glm::vec3 position;
glm::vec3 normal;
};
Объявление класса поверхности будет похоже на объявление класса CIdentitySphere, рассмотренного ранее:
// Класс поверхности "Зонтик Уитни"
// https://en.wikipedia.org/wiki/Whitney_umbrella
class CWhitneyUmbrella
{
public:
CWhitneyUmbrella(unsigned slices, unsigned stacks);
void Draw()const;
private:
void Tesselate(unsigned slices, unsigned stacks);
std::vector<SVertexP3N> m_vertices;
std::vector<uint32_t> m_indicies;
};
Реализации методов уже были рассмотрены ранее на примере сферы, мы затронем только два отличия Зонтика Уитни от сферы. Во-первых, здесь используется другая функция получения точки (x,y,z) по (u,v) координатам:
glm::vec3 GetSurfacePoint(float u, float v)
{
// Приводим параметры из диапазона [0..1] к диапазону [-3..3]
u = 6.f * (u - 0.5f);
v = 6.f * (v - 0.5f);
return { u * v, u, v * v };
}
Во-вторых, для единичной сферы нормали рассчитываются простейшим способом, потому что любой вектор радиуса от центра к точке на сфере уже является нормалью сферы. Для произвольной поверхности можно применить численный метод на основе векторного произведения сторон треугольника, касающегося поверхности в заданной точке:
void CWhitneyUmbrella::Tesselate(unsigned slices, unsigned stacks)
{
assert((slices >= MIN_PRECISION) && (stacks >= MIN_PRECISION));
m_vertices.reserve(slices * stacks);
// вычисляем позиции вершин.
for (unsigned ci = 0; ci < slices; ++ci)
{
const float u = (float(ci) / float(slices - 1));
for (unsigned ri = 0; ri < stacks; ++ri)
{
const float v = (float(ri) / float(stacks - 1));
SVertexP3N vertex;
vertex.position = GetSurfacePoint(u, v);
// Нормаль к поверхности можно рассчитать численным методом,
// для этого достаточно вычислить значение функции, задающей
// преобразование (u, v)->(x, y, z), для (u + delta, v) и
// (u, v + delta), а затем вычислить векторное произведение
// сторон полученного треугольника
glm::vec3 dir1 = GetSurfacePoint(u + UV_DELTA, v) - vertex.position;
glm::vec3 dir2 = GetSurfacePoint(u, v + UV_DELTA) - vertex.position;
vertex.normal = glm::normalize(glm::cross(dir1, dir2));
m_vertices.push_back(vertex);
}
}
CalculateTriangleStripIndicies(m_indicies, slices, stacks);
}
Шейдер twist.vert
Представленный шейдер будет использовать uniform-переменную TWIST, которая определяет коэффициент закручивания. Если TWIST равно 0, закручивание не происходит.
uniform float TWIST;
void main()
{
// Calculate rotation angle
float angle = gl_Vertex.y * TWIST;
// calculate sin(angle) and cos(angle)
float sa = sin(angle);
float ca = cos(angle);
/*
Rotate vertex around Y axis:
x' = x * cos(angle) - z * sin(angle)
y' = y;
z' = x * sin(angle) + z * cos(angle);
w' = w;
*/
vec4 twistedCoord = vec4(
gl_Vertex.x * ca - gl_Vertex.z * sa,
gl_Vertex.y,
gl_Vertex.x * sa + gl_Vertex.z * ca,
gl_Vertex.w
);
vec4 position = gl_ModelViewProjectionMatrix * twistedCoord;
// Transform twisted coordinate
gl_Position = position;
// Calculate color to add shades on surface
gl_FrontColor = (position + vec4(1.0)) * 0.5;
}
Передача параметров шейдерной программе через uniform-переменные
Шейдер может получить доступ к состоянию OpenGL через встроенные uniform-переменные, начинающиеся с префикса «gl_». Например, к текущей матрице моделирования-вида можно обратиться по имени gl_ModelViewMatrix. Приложение также могут определять свои uniform-переменные и использовать специальные команды OpenGL для установки их значений. Общее количество встроенных uniform-переменных, доступных вершинному и фрагментному процессорам, не может быть больше некоторого установленного реализацией OpenGL максимума, задаваемого в компонентах размера float (тип vec2, например, состоит из двух компонентов типа float).
Следующий код выводит количество uniform-компонентов, доступных вершинному и фрагментному процессору:
// Определение класса
class CProgramInfo
{
public:
static int GetMaxVertexUniforms();
static int GetMaxFragmentUniforms();
};
// Определение методов
int CProgramInfo::GetMaxVertexUniforms()
{
GLint result;
glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &result);
return result;
}
int CProgramInfo::GetMaxFragmentUniforms()
{
GLint result;
glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, &result);
return result;
}
Например, видеокарта nVidia GeForce 7600 GS поддерживает не более 1024 uniform-компонентов для вершинного шейдера и не более 2048 uniform-компонентов для фрагментного шейдера. Много это или мало? Рассмотрим несколько примеров:
- через 2048 uniform-компонент можно передать фрагментному шейдеру массив коэффициентов для огромного фильтра свертки размером 45x45.
- через 1024 uniform-компоненты можно передать вершинному шейдеру, выполняющему скелетную анимацию трехмерной модели, информацию о, примерно, 80 матрицах размером 4*3, задающих трансформацию 80 «костей» скелета, чего более чем достаточно для анимирования человекообразных существ.
Получаем доступ к переменной TWIST
В приведённом выше шейдере объявлена uniform-переменная TWIST, задающая коэффициент закручивания вершины в зависимости от ее координаты y. Для того, чтобы передать шейдеру значение данной uniform-переменной, необходимо определить целочисленное «расположение» (англ. «location») этой переменной. По своей роли целочисленное «расположение» похоже на указатель или итератор в языке C++, и точно так же оно предоставляет доступ к переменной на чтение и на запись.
Есть несколько способов определить «расположение». Рассмотрим два способа:
- можно после компоновки программы вызывать функцию glGetUniformLocation, передав ей дескриптор программного объекта и строку, задающую имя uniform-переменной. Если такой переменной нет среди активных uniform-переменных программы, либо имя начинается с зарезервированного префикса «gl_», функция вернет значение, равное -1.
- начиная с OpenGL 4.3, программисту доступен явный выбор расположения переменной (opengl.org), который объявляется следующим образом:
// Requires OpenGL 4.3+ !!
layout(location = 2) uniform mat4 modelToWorldMatrix;
Поскольку OpenGL 4.3 доступен далеко не на всех системах, мы воспользуемся функцией glGetUniformLocation
, и добавим два новых метода в класс CShaderProgram.
Также следует заметить, что glGetUniformLocation
возвращает расположение только для активных пользовательских переменных. Uniform-переменная является активной, если ее значение используется в шейдере. Компилятор может отбросить некоторые переменные, если в процессе компиляции выяснится, что их значение не оказывает никакого влияния на работу шейдера.
Изменения в CShaderProgram
class CShaderProgram : private boost::noncopyable
{
public:
// конструктор и методы для сборки
CProgramUniform FindUniform(const char *name)const;
// остальные методы и данные
};
CProgramUniform CShaderProgram::FindUniform(const char *name) const
{
int location = glGetUniformLocation(m_programId, name);
if (location == -1)
{
throw std::invalid_argument("Wrong shader variable name: " + std::string(name));
}
return CProgramUniform(location);
}
Для уменьшения количества вызовов к API OpenGL можно создать кэш расположений uniform-переменных, связанный с одной шейдерной программой. Для этого добавим поле mutable std::map<std::string, int> m_uniformLocationCache
к классу CShaderProgram, и модифицируем метод FindUniform:
CProgramUniform CShaderProgram::FindUniform(const std::string &name) const
{
auto cacheIt = m_uniformLocationCache.find(name);
int location = 0;
if (cacheIt != m_uniformLocationCache.end())
{
location = cacheIt->second;
}
else
{
location = glGetUniformLocation(m_programId, name.c_str());
if (location == -1)
{
throw std::invalid_argument("Wrong shader variable name: " + std::string(name));
}
m_uniformLocationCache[name] = location;
}
return CProgramUniform(location);
}
Класс CProgramUniform оборачивает целочисленное расположение переменной и предоставляет средства для установки значения переменной. Структуру этого класса мы рассмотрим позже.
Получаем информацию об активных uniform-переменных программы
Разработаем класс CProgramInfo, в котором будет размещаться функционал по сбору и выводу информации о программе.
#pragma once
#include <string>
class CProgramInfo
{
public:
static int GetMaxVertexUniforms();
static int GetMaxFragmentUniforms();
explicit CProgramInfo(unsigned programId);
/// Возвращает количество uniform-переменных в активной программе.
unsigned GetUniformCount()const;
/// Выводит информацию о uniform-переменной в поток stream.
void PrintUniformInfo(unsigned index, std::ostream &stream)const;
/// Выводим информацию о всех uniform-переменных программы.
void PrintProgramInfo(std::ostream &stream)const;
private:
unsigned m_programId = 0;
};
В конструкторе мы просто сохраняем идентификатор программы, а в методе GetUniformCount запрашиваем число активных (т.е. сохранившихся после компиляции) пользовательских uniform-переменных с помощью функции из семейства функций glGetProgram*, получающих значения свойств программы.
CProgramInfo::CProgramInfo(unsigned programId)
: m_programId(programId)
{
}
unsigned CProgramInfo::GetUniformCount() const
{
GLint count = 0;
glGetProgramiv(m_programId, GL_ACTIVE_UNIFORMS, &count);
return unsigned(count);
}
Метод вывода информации о uniform-переменной с заданным индексом в поток вывода.
void CProgramInfo::PrintUniformInfo(unsigned index, std::ostream &stream) const
{
GLint uniformArraySize = 0;
GLenum uniformType = 0;
char nameBuffer[256];
GLsizei nameLength = 0;
glGetActiveUniform(m_programId, index, GLsizei(sizeof(nameBuffer)),
&nameLength, &uniformArraySize, &uniformType,
reinterpret_cast<GLchar *>(nameBuffer));
std::string name(nameBuffer, size_t(nameLength));
// Выводим имя и тип переменной.
stream << TypeToString(uniformType) << " " << name;
// Если это массив, выводим его размер.
if (uniformArraySize != 1)
{
stream << "[" << uniformArraySize << "]";
}
// Если это не встроенная переменная, то выводим ее расположение
if (name.length() > 3 && name.substr(0, 3) != "gl_")
{
GLint location = glGetUniformLocation(m_programId, name.c_str());
stream << " at " << location;
}
}
Вспомогательная функция TypeToString переводит целочисленный идентификатор типа данных GLSL в строковое название этого типа:
namespace
{
// Преобразует идентификатор типа данных GLSL в строку
std::string TypeToString(GLenum type)
{
const std::pair<GLenum, const char *> TYPE_MAPPING[] =
{
{GL_FLOAT, "float"},
{GL_FLOAT_VEC2, "vec2"},
{GL_FLOAT_VEC3, "vec3"},
{GL_FLOAT_VEC4, "vec4"},
{GL_INT, "int"},
{GL_INT_VEC2, "ivec2"},
{GL_INT_VEC3, "ivec3"},
{GL_INT_VEC4, "ivec4"},
{GL_BOOL, "bool"},
{GL_BOOL_VEC2, "bvec2"},
{GL_BOOL_VEC3, "bvec3"},
{GL_BOOL_VEC4, "bvec4"},
{GL_FLOAT_MAT2, "mat2"},
{GL_FLOAT_MAT3, "mat3"},
{GL_FLOAT_MAT4, "mat4"},
{GL_FLOAT_MAT2x3, "mat2x3"},
{GL_FLOAT_MAT2x4, "mat2x4"},
{GL_FLOAT_MAT3x2, "mat3x2"},
{GL_FLOAT_MAT3x4, "mat3x4"},
{GL_FLOAT_MAT4x2, "mat4x2"},
{GL_FLOAT_MAT4x3, "mat4x3"},
{GL_SAMPLER_1D, "sampler1D"},
{GL_SAMPLER_2D, "sampler2D"},
{GL_SAMPLER_3D, "sampler3D"},
{GL_SAMPLER_CUBE, "samplerCube"},
{GL_SAMPLER_1D_SHADOW, "sampler1DShadow"},
{GL_SAMPLER_2D_SHADOW, "sampelr2DShadow"}
};
for (const auto &pair : TYPE_MAPPING)
{
if (pair.first == type)
{
return pair.second;
}
}
throw std::invalid_argument("Unknown variable type " + std::to_string(type));
}
}
И, наконец, общий метод, печатающий информацию о программе в поток. Эту информацию легко перенаправить в стандартный поток вывода или стандартный поток ошибок, передав в метод параметр std::cout
или std::cerr
соответственно:
void CProgramInfo::PrintProgramInfo(std::ostream &stream) const
{
const GLuint uniformCount = GetUniformCount();
stream << "Program id: " << m_programId << "\n";
stream << " Active uniform count: " << uniformCount << "\n";
for (GLuint uniform = 0; uniform < uniformCount; ++uniform)
{
stream << " ";
PrintUniformInfo(uniform, stream);
stream << "\n";
}
}
Наш класс CShaderProgram не позволяет напрямую получить целочисленный id программы. Вместо этого мы добавим метод, конструирующий и возвращающий объект CProgramInfo по значению. Передача по значению оправдана, т.к. CProgramInfo не имеет виртуальных методов, имеет тривиальный конструктор и хранит только число типа unsigned
, т.к. размер экземпляра типа равен 4 байтам на большинстве платформ.
CProgramInfo CShaderProgram::GetProgramInfo() const
{
return CProgramInfo(m_programId);
}
Подключим получение и вывод информации о программе в конструкторе класса CWindowClient после компоновки шейдерной программы:
const std::string twistShader = CFilesystemUtils::LoadFileAsString("res/twist.vert");
m_programTwist.CompileShader(twistShader, ShaderType::Vertex);
m_programTwist.Link();
std::cerr << "-- TWIST program info ---" << std::endl;
CProgramInfo info = m_programTwist.GetProgramInfo();
info.PrintProgramInfo(std::cerr);
std::cerr << "-------------------------" << std::endl;
После запуска получаем следующий вывод:
-- TWIST program info ---
Program id: 1
Active uniform count: 2
mat4 gl_ModelViewProjectionMatrixTranspose
float TWIST at 0
-------------------------
Установка значения uniform-переменной
Запрашивать информацию о программе или расположение uniform-переменных можно сразу после того, как программа была скомпонована (англ. linked). Однако, получать и устанавливать значения uniform-переменных можно только после вызова glUseProgram всё время, пока программа используется.
Класс CProgramUniform служит только для записи uniform-переменной без чтения. API OpenGL позволяет узнать установленное ранее значение, но мы используем OpenGL как средство вывода графических данных и не нуждаемся в обратной связи в данном случае. Если же потребуется отладить работу с OpenGL, отладчик APITrace позволит отследить состояние uniform-переменных.
Класс CProgramUniform объявлен следующим образом:
#pragma once
#include <glm/fwd.hpp>
class CShaderProgram;
class CProgramUniform
{
public:
explicit CProgramUniform(int location);
void operator =(int value);
void operator =(float value);
void operator =(const glm::vec2 &value);
void operator =(const glm::ivec2 &value);
void operator =(const glm::vec3 &value);
void operator =(const glm::vec4 &value);
void operator =(const glm::mat3 &value);
void operator =(const glm::mat4 &value);
// Блокируем случайное использование других типов.
void operator =(bool) = delete;
void operator =(double value) = delete;
void operator =(unsigned value) = delete;
void operator =(const void *) = delete;
private:
int m_location = -1;
};
В реализации этого класса каждый оператор присваивания вызывает нужную функцию из семейства функций glUniform*:
CProgramUniform::CProgramUniform(int location)
: m_location(location)
{
}
void CProgramUniform::operator =(int value)
{
glUniform1i(m_location, value);
}
void CProgramUniform::operator =(float value)
{
glUniform1f(m_location, value);
}
void CProgramUniform::operator =(const glm::vec2 &value)
{
glUniform2fv(m_location, 1, glm::value_ptr(value));
}
void CProgramUniform::operator =(const glm::ivec2 &value)
{
glUniform2iv(m_location, 1, glm::value_ptr(value));
}
void CProgramUniform::operator =(const glm::vec3 &value)
{
glUniform3fv(m_location, 1, glm::value_ptr(value));
}
void CProgramUniform::operator =(const glm::vec4 &value)
{
glUniform4fv(m_location, 1, glm::value_ptr(value));
}
void CProgramUniform::operator =(const glm::mat3 &value)
{
glUniformMatrix3fv(m_location, 1, false, glm::value_ptr(value));
}
void CProgramUniform::operator =(const glm::mat4 &value)
{
glUniformMatrix4fv(m_location, 1, false, glm::value_ptr(value));
}
Установим значение uniform-переменной TWIST в 0.5f
и выведем Зонтик Уитни с использованием шейдера twist.vert
в режиме Wireframe:
m_programTwist.Use();
CProgramUniform twist = m_programTwist.FindUniform("TWIST");
twist = 0.5f;
// Если программа активна, используем её и рисуем поверхность
// в режиме Wireframe.
if (m_programEnabled)
{
m_programTwist.Use();
CProgramUniform twist = m_programTwist.FindUniform("TWIST");
twist = m_twistController.GetCurrentValue();
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
m_umbrellaObj.Draw();
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
else
{
m_programFixed.Use();
m_umbrellaObj.Draw();
}
Получим такое изображение:
Плавное изменения uniform-переменной TWIST
Мы добавим вспомогательный класс, чтобы позволить пользователю программы с помощью клавиш. Класс предоставляет методы Update, OnKeyDown и метод для получения текущего значения параметра twist:
#pragma once
#include <SDL2/SDL_events.h>
class CTwistValueController
{
public:
void Update(float deltaSeconds);
bool OnKeyDown(const SDL_KeyboardEvent &event);
float GetCurrentValue()const;
private:
float m_currentTwistValue = 0;
float m_nextTwistValue = 0;
};
В полях m_nextTwistValue и m_currentTwistValue хранится соответственно ожидаемое и догоняющее значения twist. Это позволяет сделать плавную анимацию изменения закручивания с помощью небольшого обновления m_currentTwistValue
на каждом кадре. В то же время поле m_nextTwistValue
хранит мгновенно изменяемое значение twist, которое увеличивается при нажатии клавиши “+” и уменьшается при нажатии “-“.
Методы OnKeyDown и GetCurrentValue реализованы просто:
namespace
{
const float MIN_TWIST = -2.f;
const float MAX_TWIST = 2.f;
const float NEXT_TWIST_STEP = 0.2f;
const float TWIST_CHANGE_SPEED = 1.f;
}
bool CTwistValueController::OnKeyDown(const SDL_KeyboardEvent &event)
{
switch (event.keysym.sym)
{
// обрабатываем "=", потому что клавиша "+" - это "="+Shift
case SDLK_EQUALS:
case SDLK_PLUS:
m_nextTwistValue = std::min(m_nextTwistValue + NEXT_TWIST_STEP, MAX_TWIST);
return true;
case SDLK_MINUS:
m_nextTwistValue = std::max(m_nextTwistValue - NEXT_TWIST_STEP, MIN_TWIST);
return true;
default:
return false;
}
}
float CTwistValueController::GetCurrentValue() const
{
return m_currentTwistValue;
}
Метод Update реализует основную фишку класса: если значения m_currentTwistValue и m_nextTwistValue различаются, то значение m_currentTwistValue чуть-чуть изменится в сторону m_nextTwistValue, при этом прирост значения зависит от параметра метода deltaSeconds
:
// При каждом вызове Update величина twist "догоняет" назначенное значение.
void CTwistValueController::Update(float deltaSeconds)
{
const float twistDiff = fabsf(m_nextTwistValue - m_currentTwistValue);
if (twistDiff > std::numeric_limits<float>::epsilon())
{
const float sign = (m_nextTwistValue > m_currentTwistValue) ? 1 : -1;
const float growth = deltaSeconds * TWIST_CHANGE_SPEED;
if (growth > twistDiff)
{
// расчётный прирост выше, чем реальная разница
// между значениями, и мы просто присваиваем
m_currentTwistValue = m_nextTwistValue;
}
else
{
// прибавляем небольшой прирост к текущему значению twist.
m_currentTwistValue += sign * growth;
}
}
}
Результат
Программа обрабатывает три способа для управления шейдером:
- ”+” (“=”) увеличивает параметр закручивания
- ”-“ уменьшает параметр закручивания
- “Пробел” отключает шейдер закручивания и включает фиксированный конвейер
Для удобства информация о сочетаниях клавиш добавлена в заголовок окна, определяемый в функции main
:
int main(int, char *[])
{
try
{
CWindow window;
window.Show("OpenGL Demo (+/- to control twist, SPACE to disable shader)", {800, 600});
CWindowClient client(window);
window.DoMainLoop();
}
catch (const std::exception &ex)
{
const char *title = "Fatal Error";
const char *message = ex.what();
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr);
}
return 0;
}
После запуска программы мы получаем следующее: