Рубрики
3D мир Программирование

OpenGL 17: выбор объектов и элементы управления мышью

Заметка посвящена определению нажатия на объект путем записи идентификатора в текстуру и манипуляциям с объектом с помощью инструментов.

Введение

Одним из способов взаимодействия с приложениями, использующими трехмерную графику, является курсор мыши, который может использоваться для выделения объектов, их перемещения или вызова контекстного меню.

Существует два способа определения объекта по координатам в пространстве окна:

  • проверка пересечения объекта с лучом, выпущенным из камеры в направлении курсора мыши;
  • запись идентификаторов объектов в текстуру и определение по цвету пикселя из текстуры.

В заметке будет рассмотрен второй способ.

Содержание заметки:

Выбор модели наведением курсора

Как было сказано ранее: для определения нужного объекта будут использоваться идентификаторы объектов, записанные в текстуру. В качестве идентификатора может выступать уникальное целое беззнаковое число, позволяющее однозначно идентифицировать объект.

Для простоты работы с идентификаторами можно использовать адрес объекта в памяти: если значение пикселя в текстуре имеет не нулевое значение, значит на данной текстуре изображен объект, расположенный по данному адресу. Адрес в качестве идентификатора удобен тем, что он уникальный для каждого объекта и позволяет без дополнительного поиска обратиться к объекту, но имеет большой минус на х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.

Рисунок 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.fragshaders/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.

Рисунок 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.

Рисунок 3 — Перемещение отладочной лампочки

Текущая версия доступна на теге v0.5 в репозитории 17.

Заключение

В заметке были рассмотрены инструменты взаимодействия с объектами сцены (перемещение, вращение и масштабирование), рассмотрена запись идентификатора в текстуру для определения нажатия на объект.

Проект доступен в публичном репозитории: 17
Библиотеки: dependencies
Ресурсы: resources

4 ответа к “OpenGL 17: выбор объектов и элементы управления мышью”

Спасибо за комментарий!
Да, в планах скелетная анимация, frustum culling, сохранение и загрузка сцен, а так же их графы. Потом может будет пара заметок по оптимизации, после будет физика.
Вообще я открыт для пожеланий читателей.

Здравствуйте.
Будет ли материал про объектный менеджер, который аккуратно создаёт, удаляет и использует объекты, как в QT, например? Подводки к «Менеджеру памяти» были в третьей статье по теме

В данной реализации работа с ресурсами (текстурами и vbo) является динамической — освобождение происходит при удалении последнего владельца.
Создание объектов идёт на откуп загрузчику сцен дабы минимизировать загрузки моделей с диска

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Управление cookie