Введение
Одним из способов взаимодействия с приложениями, использующими трехмерную графику, является курсор мыши, который может использоваться для выделения объектов, их перемещения или вызова контекстного меню.
Существует два способа определения объекта по координатам в пространстве окна:
- проверка пересечения объекта с лучом, выпущенным из камеры в направлении курсора мыши;
- запись идентификаторов объектов в текстуру и определение по цвету пикселя из текстуры.
В заметке будет рассмотрен второй способ.
Содержание заметки:
- выбор модели наведением курсора;
- модификация обработчика мыши;
- обводка вокруг выбранного объекта;
- инструменты для редактирования сцены;
- модификация класса источника света.
Выбор модели наведением курсора
Как было сказано ранее: для определения нужного объекта будут использоваться идентификаторы объектов, записанные в текстуру. В качестве идентификатора может выступать уникальное целое беззнаковое число, позволяющее однозначно идентифицировать объект.
Для простоты работы с идентификаторами можно использовать адрес объекта в памяти: если значение пикселя в текстуре имеет не нулевое значение, значит на данной текстуре изображен объект, расположенный по данному адресу. Адрес в качестве идентификатора удобен тем, что он уникальный для каждого объекта и позволяет без дополнительного поиска обратиться к объекту, но имеет большой минус на х64-битной архитектуре — адрес занимает 8 байт (RG компоненты цвета), что по сравнению с х32 (только R компонент) требует дополнительный компонент цвета. Для дополнительной информации, которая потребуется для работы с инструментами будет использован третий компонент B.
Создадим и привяжем текстуру gID к буферу отложенного рендера gbuffer в файле src/main.cpp:
// Создадим G-буфер с данными о используемых привязках
GLuint attachments[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4 };
FBO gbuffer(attachments, sizeof(attachments) / sizeof(GLuint));
// Создадим текстуры для буфера кадра
...
Texture gAmbientSpecular(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT3, 3); // Фоновая составляющая и один канал зеркальной
Texture gID(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT4, 7, GL_RGB32UI, GL_RGB_INTEGER, GL_UNSIGNED_INT); // Идентификатор объекта
Важное замечание: компоненты текстуры должны быть записаны целочисленными.
В обработчик окна (framebuffer_size_callback) добавим изменение размера текстуры с идентификаторами объектов:
// Указатели на текстуры для изменения размеров окна
Texture* pgID = NULL;
...
// Функция-callback для изменения размеров буфера кадра в случае изменения размеров поверхности окна
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
...
if (pgID)
pgID->reallocate(width, height, 7, GL_RGB32UI, GL_RGB_INTEGER, GL_UNSIGNED_INT);
...
В функции main запишем адрес после создания текстуры:
// Сохраним указатели на текстуры для изменения размеров окна
pgPosition = &gPosition;
pgNormal = &gNormal;
pgDiffuseP = &gDiffuseP;
pgAmbientSpecular = &gAmbientSpecular;
pgrbo = &grbo;
pgID = &gID;
Дополним фрагментный шейдер для работы с буфером отложенного рендера shaders/gshader.frag, добавив текстуру для записи, uniform-переменную и перенос её значения в текстуру:
...
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gDiffuseP;
layout (location = 3) out vec4 gAmbientSpecular;
layout (location = 4) out uvec3 gID;
...
uniform uvec3 ID = uvec3(0);
void main()
{
...
// Сохранение зеркальной составляющей
gAmbientSpecular.a = texture(tex_specular, new_texCoord).r * ks.r;
// Сохранение идентификатора объекта
gID = ID;
}
Необходимо дополнить класс Model функциональностью записи идентификатора в uniform-переменную шейдера в методе Model::render.
В файле include/Model.h добавим структуру ID, состоящую из двух полей:
// Идентификатор модели
struct ID
{
GLuint64 value = 0; // Идентификатор
GLuint etc = 0; // Дополнительная информация
};
Примечание: сейчас поле ID::etc не используется, но позднее будет использовано для инструментов.
Тогда для классов будут следующие изменения:
class Model : public Node
{
public:
...
ID id; // ID модели
...
};
В файле src/Model.cpp дополним конструкторы:
// Конструктор без параметров
Model::Model() : verteces_count(0), ...
{
// Приведение указателя к целому 8байт
id.value = (GLuint64) this;
id.etc = 0;
}
// Конструктор копирования
Model::Model(const Model& copy) : Node(copy),
...
{
// Приведение указателя к целому 8байт
id.value = (GLuint64) this;
id.etc = copy.id.etc;
}
В метод Model::render добавим загрузку идентификатора:
void Model::render(ShaderProgram &shaderProgram, UBO &material_buffer, const glm::mat4& global_tranform)
{
// Загрузка идентификатора объекта
glUniform3uiv(shaderProgram.getUniformLoc("ID"), 1, (GLuint*) &id);
...
Для класса Scene добавим метод set_group_id для изменения идентификаторов связанных моделей в файле include/Scene.h:
// Класс сцены
class Scene
{
public:
...
void set_group_id(GLuint64 id, GLuint etc = 0); // Изменение флага записи идентификатора для всех моделей
...
};
А так же добавим реализацию метода Scene::set_write_id в файле src/Scene.cpp:
// Изменение флага записи идентификатора для всех моделей
void Scene::set_group_id(GLuint64 id, GLuint etc)
{
for (auto& model : models)
{
model.id.value = id;
if (etc)
model.id.etc = etc;
}
}
Такой подход позволяет записывать адрес в качестве идентификатора как сгруппированных объектов, так и индивидуальные адреса каждой модели из группы, что увеличивает гибкость сцены.
Теперь в файле src/main.cpp получим идентификатор по координатам курсора. Создадим объект структуры ID с именем selected перед циклом рисования и заполним данную структуру после рисования в буфер отложенного рендера:
ID selected; // Выбранная модель
// Пока не произойдет событие запроса закрытия окна
while(!glfwWindowShouldClose(window))
{
...
// Тут производится рендер
scene.render(gShader, material_data);
rectangle.render(gShader, material_data);
// Выбор объекта
glReadBuffer(GL_COLOR_ATTACHMENT4);
glReadPixels(lastX, WINDOW_HEIGHT-lastY, 1, 1, GL_RG_INTEGER, GL_UNSIGNED_INT, &selected);
std::cout << (void*) selected.value << ' ' << selected.etc << '\n';
// Активируем буфер SSAO
...
Теперь при наведении мыши на объект, отрендеренный в буфер отложенного рендера, он будет выбран. При наведении мыши на свободное пространство ID принимает значение 0 (NULL).
Текущая версия доступна на теге v0.1 в репозитории 17.
Модификация обработчика мыши
На данный момент в приложении задействован простейший обработчик мыши, который не является удобным для работы с камерой и выбора объектов.
Для упрощения работы с мышью добавим новую структуру Mouse с экземпляром mouse перед функцией-обработчиком события движения, удалив переменные lastX и lastY, в файле src/main.cpp:
float lastX, lastY;
struct Mouse
{
float x = 0, y = 0; // Координаты курсора
float prev_x = 0, prev_y = 0; // Координаты курсора на предыдущем кадре
uint16_t left = 040100, right = 040100; // Состояние кнопок
} mouse;
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
Пусть Mouse::x и Mouse::y — последние координаты курсора, изменяемые функцией mouse_callback, Mouse::prev_x и Mouse::prev_y — координаты на предыдущем кадре, а Mouse::left и Mouse::right — поля, отвечающие за состояние кнопок мыши, которые могут принимать следующие значения:
- 1638410 (0400008) — кнопка отпущена;
- 3276810 (01000008) — кнопка нажата;
- 1638310 (0377778) — максимальное количество кадров с последнего изменения состояния.
Идея заключается в том, что два старших бита в двухбайтовом беззнаковом целом отводятся под последнее состояние: кнопка нажата или отпущена, а оставшиеся биты отвечают за количество кадров, прошедших с последнего изменения. Например значение 0400008 означает что кнопка была отпущена только что, а значения 0400018 и 0400058 — кнопка отпущена один и пять кадров назад соответственно. Требуется каждый кадр изменять значение данного счетчика с помощью вспомогательной функции process_mouse_button.
Важное замечание: важно не допустить выход за предел максимального количества кадров с последнего изменения состояния. Автор предлагает следующее решение — добавить проверку на равенство со значением максимального количества кадров и накладывать маску 01401008, которая позволит сохранить последнее состояние кнопок, а так же откатить счетчик на середину.
Пример функции process_mouse_button из файла src/main.cpp:
void process_mouse_button(uint16_t& button)
{
if ((++button & 037777) == 037777)
button &= 0140100;
}
Такой подход позволит определять продолжительность нажатия, длительность промежутка между двумя нажатиями.
Данную функцию и запись координат мыши для следующего кадра требуется добавить в конец цикла рисования, перед обработкой событий:
// Дополнительная обработка мыши
process_mouse_button(mouse.left);
process_mouse_button(mouse.right);
mouse.prev_x = mouse.x;
mouse.prev_y = mouse.y;
// Представление содержимого буфера цепочки показа на окно
glfwSwapBuffers(window);
// Обработка системных событий
glfwPollEvents();
Добавим функцию-обработчик кнопок мыши mouse_button_callback в файл src/main.cpp:
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
uint16_t& mouse_button = (button == GLFW_MOUSE_BUTTON_LEFT)?mouse.left:mouse.right;
if (action == GLFW_PRESS && !(mouse_button & 0100000))
mouse_button = 0100000;
else if (action == GLFW_RELEASE)
mouse_button = 040000;
}
Важное замечание: движение с зажатой кнопкой мыши генерируют новые события GLFW_PRESS, что может приводить к обнулению счетчика кадров, чтобы этого избежать — производится дополнительная проверка на то что кнопка еще не нажата.
Данная функция и структура являются легко масштабируемыми на любое число кнопок, но максимально GLFW3 поддерживает 8 кнопок: GLFW_MOUSE_BUTTON_LEFT (GLFW_MOUSE_BUTTON_1), GLFW_MOUSE_BUTTON_RIGHT (GLFW_MOUSE_BUTTON_2), GLFW_MOUSE_BUTTON_MIDDLE (GLFW_MOUSE_BUTTON_3), GLFW_MOUSE_BUTTON_4, …, GLFW_MOUSE_BUTTON_8.
Подключим данную функцию в качестве обработчика с помощью функции glfwSetMouseButtonCallback:
// Установка callback-функции для мыши
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetMouseButtonCallback(window, mouse_button_callback);
Теперь можно сильно упростить функцию-обработчик движений мышью — mouse_callback, убрав поворот камеры:
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
mouse.x = xpos;
mouse.y = ypos;
}
Добавим поворот камеры в конец цикла рисования после обработки событий:
...
// Представление содержимого буфера цепочки показа на окно
glfwSwapBuffers(window);
// Обработка системных событий
glfwPollEvents();
if (mouse.right & 0100000
&& mouse.x != mouse.prev_x
&& mouse.y != mouse.prev_y)
camera.rotate(glm::vec2(mouse.x - mouse.prev_x, mouse.prev_y - mouse.y));
}
Осталось только сделать выбор объекта по нажатию левой кнопки мыши, ограничив чтение с текстуры при нажатии левой кнопки мыши:
// Выбор объекта
if (mouse.left == 0100000)
{
glReadBuffer(GL_COLOR_ATTACHMENT4);
glReadPixels(mouse.x, WINDOW_HEIGHT-mouse.y, 1, 1, GL_RG_INTEGER, GL_UNSIGNED_INT, &selected);
std::cout << (void*) selected.value << ' ' << selected.etc << '\n';
}
Примечание: можно назначить выбор объекта на отжатие кнопки мыши, сравнив значение левой кнопки мыши со значением 040000.
Текущая версия доступна на теге v0.2 в репозитории 17.
Обводка вокруг выбранного объекта
Для визуального отображения выбранного объекта нарисуем белую обводку-границу, дополнив шейдер освещения. Для этого подключим к шейдеру текстуру идентификаторов:
// Привязка текстур
const char* gtextures_shader_names[] = {"gPosition", "gNormal", "gDiffuseP", "gAmbientSpecular", "sunShadowDepth", "pointShadowDepth", "ssao", "gID"};
В фрагментном шейдере shaders/lighting.frag добавим текстуру и uniform-переменную:
uniform usampler2D gID;
uniform uvec3 selectedID;
А так же добавим проверку цветов в текстуре с идентификаторами после применения гамма-коррекции:
...
// Применение гамма-коррекции
color.rgb = pow(color.rgb, vec3(inv_gamma));
vec3 ID = texture(gID, texCoord).rgb;
// Обводка выбранного объекта
if (length(selectedID.rg) > 0 && selectedID.rg == texture(gID, texCoord).rg)
{
int border_width = 3;
vec2 size = 1.0f / textureSize(gID, 0);
for (int i = -border_width; i <= +border_width; i++)
for (int j = -border_width; j <= +border_width; j++)
{
if (i == 0 && j == 0)
continue;
if (texture(gID, texCoord + vec2(i, j) * size).rg != selectedID.rg)
color.rgb = vec3(1.0);
}
}
}
Идея данного алгоритма заключается в том, что в случае, если есть ненулевой идентификатор (selectedID.rg) и рассматриваемый фрагмент (texture(gID, texCoord).rg или вынесенный в ID.rg) соответствует данному идентификатору, то следует взять пробы заданной ширины (border_width) в цикле и в случае несоответствия идентификаторов — окрасить в белый цвет. Дополнительно производится проверка на то, что дополнительная информация идентификатора (etc, записанный в ID.b) равна нулю, что позволяет обводить только объект, а не инструменты, взаимодействующие с ним.
Осталось только подключить текстуру и загрузить выбранный идентификатор в uniform-переменную перед вычислением освещения в файле src/main.cpp:
...
// Подключаем шейдер для прямоугольника
lightShader.use();
// Очистка буфера цвета и глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Подключаем текстуры G-буфера
gPosition.use();
gNormal.use();
gDiffuseP.use();
gAmbientSpecular.use();
gID.use();
// Идентификатор выбранного объекта для обводки
glUniform3uiv(lightShader.getUniformLoc("selectedID"), 1, (GLuint*) &selected);
// Подключаем текстуры теней
...
Результат выбора объекта с последующим его обведением представлен на рисунке 1.
Текущая версия доступна на теге v0.3 в репозитории 17.
Инструменты для редактирования сцены
В качестве инструментов для редактирования объектов сцены будут использованы модели из директории models/tools репозитория resources:
- transform.obj — движение по осям;
- rotate.obj — вращение по осям;
- scale.obj — масштабирование по осям.
Для удобства работы создадим класс TRS, являющийся абстрактным родителем для перечисленных инструментов.
Создадим файл include/TRS.h, в котором объявим перечисление класс TRS:
// Интерфейс инструмента
class TRS
{
public:
void render(GLuint64 selectedID, ShaderProgram &shaderProgram, UBO &material_buffer); // Рендер инструмента нужного типа для выбранного объекта
virtual void process(GLuint64 selectedID, GLuint etc, const glm::vec4& dpos) = 0; // Взаимодействие с инструментом
protected:
void init_etc(); // Инициализирует дополнительную информацию модели
Scene tool; // Модель
};
Данный класс имеет чисто-виртуальный метод TRS::process, определяющий реакцию на движение мышью для конкретного потомка, а также методы TRS::render (рисует модель инструмента TRS::tool с идентификатором объекта и на его координатах) и TRS::init_etc (заполняет дополнительную информацию сегментов модели).
Требуется создать файл src/TRS.cpp. Реализация методов класса TRS в файле src/TRS.cpp:
// Инициализирует дополнительную информацию модели
void TRS::init_etc()
{
int value = 1;
for (auto it = tool.models.begin(); it != tool.models.end(); ++it)
it->id.etc = value++;
}
// Рендер инструмента нужного типа для выбранного объекта
void TRS::render(GLuint64 selectedID, ShaderProgram &shaderProgram, UBO &material_buffer)
{
// Если есть выбранная модель - рендерим инструмент для неё
if (selectedID)
{
// Указатель на объект
Node* selectedObject = (Node*) selectedID;
// Смещение выбранного объекта в глобальных координатах из его матрицы трансформации (включая родительские)
tool.root.e_position() = glm::vec3(selectedObject->getTransformMatrix()[3]);
// Замена идентификатора инструмента идентификатором выбранного объекта
tool.set_group_id(selectedID); // без замены доп. информации
// Рендер инструмента
tool.render(shaderProgram, material_buffer);
}
}
Примечание: поле etc у идентификатора модели используется для идентификации какая именно часть инструмента используется.
От класса TRS унаследуем классы Transform, Rotate и Scale в файле include/TRS.h:
// Инструмент трансформации
class Transform : public TRS
{
public:
Transform();
virtual void process(GLuint64 selectedID, GLuint etc, const glm::vec4& dpos); // Взаимодействие с инструментом
};
// Инструмент поворота
class Rotate : public TRS
{
public:
Rotate();
virtual void process(GLuint64 selectedID, GLuint etc, const glm::vec4& drot); // Взаимодействие с инструментом
};
// Инструмент масштабирования
class Scale : public TRS
{
public:
Scale();
virtual void process(GLuint64 selectedID, GLuint etc, const glm::vec4& dscale); // Взаимодействие с инструментом
};
Для данных классов потребуется задать коэффициент, определяющий чувствительность мыши:
#define T_SENSITIVITY 0.001f
#define R_SENSITIVITY 0.01f
#define S_SENSITIVITY 0.00001f
Реализация конструкторов в файле src/TRS.cpp для созданных классов:
Transform::Transform()
{
tool = loadOBJtoScene("../resources/models/tools/transform.obj", "../resources/models/tools/", "../resources/textures/");
// Масштабирование
tool.root.e_scale() = glm::vec3(0.3);
// Инициализация дополнительной информации
init_etc();
}
Rotate::Rotate()
{
tool = loadOBJtoScene("../resources/models/tools/rotate.obj", "../resources/models/tools/", "../resources/textures/");
// Масштабирование
tool.root.e_scale() = glm::vec3(0.3);
// Инициализация дополнительной информации
int value = 1;
for (auto it = tool.models.begin(); it != tool.models.end(); ++it)
{
it->id.etc = value;
value *= 2;
}
}
Scale::Scale()
{
tool = loadOBJtoScene("../resources/models/tools/scale.obj", "../resources/models/tools/", "../resources/textures/");
// Масштабирование
tool.root.e_scale() = glm::vec3(0.3);
// Инициализация дополнительной информации
init_etc();
}
В целом поведение конструкторов отличается только в названии загружаемой модели, но для инструмента поворота требуется вручную расставить значения etc, так как инструмент имеет меньше осей для взаимодействия по сравнению с инструментами перемещения и масштабирования.
Метод взаимодействия с инструментом перемещения Transform::process требует учитывать поворот родительского узла, так как например в случае поворота родительского узла на 90° вокруг оси Х перемещения дочерних объектов вдоль осей YZ происходят не корректно. В таком случае необходимо проверить наличие родителя у выбранного объекта. Если родитель присутствует, то следует сократить матрицу трансформации до поворотов, отбросив перемещение (4 столбец) и масштабирование (нормировав оставшиеся столбцы). Данная матрица может быть использована в произведении с вектором перемещения по осям для переноса перемещения в пространство родителя, что делает перемещения по осям для объекта корректными. Реализация данного метода:
// Взаимодействие с инструментом
void Transform::process(GLuint64 selectedID, GLuint etc, const glm::vec4& dpos)
{
// Если взаимодействие с осями инструмента
if (etc > 0)
// Если есть выбранная модель - рендерим инструмент для неё
if (selectedID)
{
// Указатель на объект
Node* selectedObject = (Node*) selectedID;
glm::vec3 dVec(0.0f, 0.0f, 0.0f);
// Сдвиг с учетом чувствительности для соответствующих осей
if (etc & 01)
dVec.x = dpos.x * T_SENSITIVITY;
if (etc & 02)
dVec.y = dpos.y * T_SENSITIVITY;
if (etc & 04)
dVec.z = dpos.z * T_SENSITIVITY;
// Если есть родитель - требуется учесть его поворот
Node* parent = selectedObject->getParent();
if (parent)
{
// Извлекаем 3x3 подматрицу, отвечающую за вращение и масштаб
glm::mat3 rotationMatrix = glm::mat3(parent->getTransformMatrix());
// Нормализуем столбцы подматрицы, чтобы исключить масштабирование
for (int i = 0; i < 3; i++)
rotationMatrix[i] = glm::normalize(rotationMatrix[i]);
// Применим поворот родителя к вектору сдвига
dVec = glm::inverse(rotationMatrix) * dVec;
}
// Добавим сдвиг от инструмента к позиции выбранного объекта
selectedObject->e_position() += dVec;
}
}
Метод взаимодействия с инструментом масштабирования Scale::process идейно повторяет метод класса Transform, за малой разницей полей объекта по которым производятся изменения:
// Взаимодействие с инструментом
void Scale::process(GLuint64 selectedID, GLuint etc, const glm::vec4& dscale)
{
// Если взаимодействие с осями инструмента
if (etc > 0)
// Если есть выбранная модель - рендерим инструмент для неё
if (selectedID)
{
// Указатель на объект
Node* selectedObject = (Node*) selectedID;
// Для хранения результата
glm::vec3 dVec(0);
// Масштабирование с учетом чувствительности для соответствующих осей
if (etc & 01)
dVec.x = dscale.x * S_SENSITIVITY;
if (etc & 02)
dVec.y = dscale.y * S_SENSITIVITY;
if (etc & 04)
dVec.z = dscale.z * S_SENSITIVITY;
// Если есть родитель - требуется учесть его поворот
Node* parent = selectedObject->getParent();
if (parent)
{
// Извлекаем 3x3 подматрицу, отвечающую за вращение и масштаб
glm::mat3 rotationMatrix = glm::mat3(parent->getTransformMatrix());
// Нормализуем столбцы подматрицы, чтобы исключить масштабирование
for (int i = 0; i < 3; i++)
rotationMatrix[i] = glm::normalize(rotationMatrix[i]);
// Применим поворот родителя к вектору сдвига
dVec = glm::inverse(rotationMatrix) * dVec;
}
// Прибавим вектор масштабирования к объекту
selectedObject->e_scale() += dVec;
}
}
Метод взаимодействия с инструментом вращения Rotate::process также требует учитывать поворот родительского узла, но при этом он используется для определения положения локальных осей вокруг которых генерируются кватернионы поворота функцией glm::angleAxis:
// Взаимодействие с инструментом
void Rotate::process(GLuint64 selectedID, GLuint etc, const glm::vec4& drot)
{
// Если взаимодействие с осями инструмента
if (etc > 0)
// Если есть выбранная модель - рендерим инструмент для неё
if (selectedID)
{
// Указатель на объект
Node* selectedObject = (Node*) selectedID;
glm::quat &selectedRot = selectedObject->e_rotation();
// Матрица родительского поворота
glm::mat3 parentRotation(1);
// Учет родительского поворота для вращения
Node* parent = selectedObject->getParent();
if (parent)
{
// Извлекаем 3x3 подматрицу, отвечающую за вращение и масштаб
parentRotation = glm::mat3(parent->getTransformMatrix());
// Нормализуем столбцы подматрицы, чтобы исключить масштабирование
for (int i = 0; i < 3; i++)
parentRotation[i] = glm::normalize(parentRotation[i]);
}
// Поворот по осям
if (etc & 01)
selectedRot = glm::angleAxis(drot.y * R_SENSITIVITY, parentRotation * glm::vec3(1.0f, 0.0f, 0.0f)) * selectedRot;
if (etc & 02)
selectedRot = glm::angleAxis(drot.x * R_SENSITIVITY, parentRotation * glm::vec3(0.0f, 1.0f, 0.0f)) * selectedRot;
if (etc & 04)
selectedRot = glm::angleAxis(drot.z * R_SENSITIVITY, parentRotation * glm::vec3(0.0f, 0.0f, 1.0f)) * selectedRot;
}
}
Для рисования инструмента потребуется измененная копия фрагментного шейдера shaders/gshader.frag — shaders/tools.frag:
#version 420 core
layout(std140, binding = 1) uniform Material
{
vec3 ka;
vec3 kd;
vec3 ks;
float p;
bool normalmapped;
bool parallaxmapped;
bool displacementmapped;
};
layout (location = 1) out vec3 gNormal;
layout (location = 3) out vec4 gAmbientSpecular;
layout (location = 4) out uvec3 gID;
in vec3 vertex; // Позиция вершины в пространстве
in vec3 N; // Нормаль трансформированная
in vec2 texCoord; // Текстурные координаты
in vec3 T; // Касательный вектор
in vec3 B; // Бикасательный вектор
in vec3 view; // Вектор от поверхности к камере
uniform uvec3 ID = uvec3(0);
void main()
{
gNormal = vec3(0);
// Сохранение фоновой составляющей
gAmbientSpecular.rgb = ka;
// Сохранение идентификатора объекта
gID = ID;
gl_FragDepth = 0.01 * gl_FragCoord.z;
}
Важная особенность данного шейдера — значение глубины фрагмента умножаются на 0.01 для обеспечения «рисования поверх модели».
Загрузим данный шейдер в файле src/main.cpp:
// Шейдер для инструментов
ShaderProgram toolsShader;
// Загрузим шейдеры
toolsShader.load(GL_VERTEX_SHADER, "shaders/gshader.vert");
toolsShader.load(GL_FRAGMENT_SHADER, "shaders/tools.frag");
toolsShader.link();
Создадим объекты инструментов и общий currentTool для удобства переключения:
// Инструменты
Transform transform;
Rotate rotate;
Scale scale;
TRS& currentTool = transform;
В цикле рисования, перед выбором объекта с помощью мыши, добавим рисование инструмента:
// Используем шейдер для инструментов
toolsShader.use();
// Рендерим инструменты для выбранного объекта
currentTool.render(selected.value, toolsShader, material_data);
// Выбор объекта
if (mouse.left == 0100000)
и в самом конце цикла обработку движений при зажатой кнопке мыши:
// Взаимодействие с инструментом при зажатой левой кнопке
if (mouse.left > 0100000)
if (selected.etc)
currentTool.process(selected.value, selected.etc, glm::transpose(camera.getVP()) * glm::vec4(mouse.x - mouse.prev_x, mouse.prev_y - mouse.y, 0, 1));
Примечание: для получения перемещения мыши в трехмерном пространстве необходимо умножить разницу смещения по осям на транспонированную матрицу вида-проекции камеры.
Результат рисования инструмента для выбранного объекта представлен на рисунке 2.
Текущая версия доступна на теге v0.4 в репозитории 17.
Модификация класса источника света
Необходимо переделать шейдер рисования отладочных лампочек (shaders/bulb.frag) на работу с буфером отложенного рендера:
#version 420 core
layout(std140, binding = 1) uniform Material
{
vec3 ka;
vec3 kd;
vec3 ks;
float p;
bool normalmapped;
bool parallaxmapped;
bool displacementmapped;
};
in vec3 pos_local;
layout(std140, binding = 4) uniform gamma
{
float inv_gamma;
};
layout (location = 1) out vec3 gNormal;
layout (location = 3) out vec4 gAmbientSpecular;
layout (location = 4) out uvec3 gID;
uniform float angle;
uniform vec3 direction;
uniform uvec3 ID = uvec3(0);
void main()
{
float cosA = dot(normalize(pos_local), normalize(direction));
if (degrees(acos(cosA)) <= angle)
gAmbientSpecular.rgb = pow(ka, vec3(inv_gamma));
else
discard;
gNormal = vec3(0);
// Сохранение идентификатора объекта
gID = ID;
}
В методе Light::render требуется задать групповой идентификатор сцене с лампочкой, указывающий на источник света в массиве источников (файл src/Lights.cpp):
// Рисование отладочных лампочек
void Light::render(ShaderProgram &shaderProgram, UBO &material_buffer)
{
// Загрузка модели лампочки при первом вызове функции
static Scene bulb = loadOBJtoScene("../resources/models/bulb.obj", "../resources/models/", "../resources/textures/");
static Model sphere = genShpere(1, 16, &bulb.root);
GLuint angle_uniform = shaderProgram.getUniformLoc("angle");
GLuint direction_uniform = shaderProgram.getUniformLoc("direction");
// Цикл по источникам света
for (int i = 0; i < count; i++)
{
// Идентификатор источника как узла сцены для всей модели лампочки
bulb.set_group_id((GLuint64) &lights[i]);
sphere.id.value = (GLuint64) &lights[i];
// Загрузим направление
glUniform3fv(direction_uniform, 1, &data[i].direction_angle.x);
// Угол для лампочки = 180 (рисуем целую модель)
glUniform1f(angle_uniform, 180); // Зададим параметры материала сфере действия
Осталось сдвинуть рисование отладочной лампочки в цикле вверх — перед рисованием инструмента:
// Отрисовка отладочных лампочек со специальным шейдером
bulbShader.use();
Light::render(bulbShader, material_data);
// Используем шейдер для инструментов
toolsShader.use();
// Рендерим инструменты для выбранного объекта
currentTool.render(selected.value, toolsShader, material_data);
Теперь инструменты могут управлять лампочками. Результат манипуляций представлен на рисунке 3.
Текущая версия доступна на теге v0.5 в репозитории 17.
Заключение
В заметке были рассмотрены инструменты взаимодействия с объектами сцены (перемещение, вращение и масштабирование), рассмотрена запись идентификатора в текстуру для определения нажатия на объект.
Проект доступен в публичном репозитории: 17
Библиотеки: dependencies
Ресурсы: resources
4 ответа к “OpenGL 17: выбор объектов и элементы управления мышью”
Классные статьи!!! Будет ли продолжение?
Спасибо за комментарий!
Да, в планах скелетная анимация, frustum culling, сохранение и загрузка сцен, а так же их графы. Потом может будет пара заметок по оптимизации, после будет физика.
Вообще я открыт для пожеланий читателей.
Здравствуйте.
Будет ли материал про объектный менеджер, который аккуратно создаёт, удаляет и использует объекты, как в QT, например? Подводки к «Менеджеру памяти» были в третьей статье по теме
В данной реализации работа с ресурсами (текстурами и vbo) является динамической — освобождение происходит при удалении последнего владельца.
Создание объектов идёт на откуп загрузчику сцен дабы минимизировать загрузки моделей с диска