Урок №17. Mesh в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 11 Дек 2020  | 

 4391

С помощью библиотеки Assimp мы можем загрузить в наше приложение самые разнообразные модели, но стоит учесть, что после загрузки все они будут сохранены в структурах данных Assimp. А это значит, что для визуализации наших загруженных объектов, нам необходимо будет преобразовать эти данные в формат, который был бы понятен для OpenGL. Из предыдущего урока мы узнали, что mesh (или «меш») — это минимальная единица отрисовки объекта, поэтому предлагаю начать с определения собственного класса Mesh.

Создание класса Mesh

Давайте немного подумаем и определимся, какие переменные для этого потребуются. Как минимум, у нас должен быть набор вершин, в котором каждая вершина включает в себя информацию о векторе положения, векторе нормали и векторе текстурных координат. Меш также должен содержать индексы для индексированной отрисовки и данные о материалах, представляемых в виде текстур (диффузные/зеркальные карты).

Теперь, когда мы установили минимальные требования для создаваемого mesh-класса, мы можем определить вершину в OpenGL:

Каждый из необходимых атрибутов вершины хранится в структуре, называемой Vertex. Ну и аналогичным образом можно организовать хранение текстурных данных в виде структуры Texture:

В ней у нас будут храниться идентификатор текстуры и её тип (например, диффузная или зеркальная).

Имея текущие представления вершины и текстуры, мы можем приступить к описанию mesh-класса:

Как вы видите, наш класс довольно простой. Для создания mesh-объекта мы передаем конструктору класса все необходимые данные, затем инициализируем буферы в функции setupMesh() и, в конце, отрисовываем меш с помощью функции Draw(). Обратите внимание на то, что функция Draw() принимает шейдер в качестве своего параметра; благодаря этому, до момента отрисовки, мы можем установить несколько uniform-переменных (например, связать сэмплеры с соответствующими текстурными юнитами).

Рассмотрим реализацию конструктора. Я думаю с этим у вас не должно возникнуть проблем, т.к. мы просто присваиваем значения аргументов конструктора соответствующим переменным класса, а затем вызываем функцию setupMesh():

Собственно, комментировать особо то и нечего, поэтому давайте углубимся внутрь функции setupMesh().

Инициализация


Благодаря конструктору у нас теперь есть большой объем mesh-данных, которые мы можем использовать в процессе визуализации. Для этого необходимо настроить соответствующие буферы и установить с помощью указателей атрибутов вершин layout-параметр вершинного шейдера. Ранее мы уже обсуждали реализацию подобного алгоритма, но на этот раз он был немного усложнен введением представления вершинных данных в качестве переменной типа структура:

Данный код почти не отличается от того, что мы встречали на предыдущих уроках. Но при этом, для работы со структурой Vertex, было задействовано несколько небольших трюков.

В языке C++ структуры имеют одно очень важное свойство — их расположение в памяти является последовательным. То есть, если представить структуру в виде массива данных, то он состоял бы только из внутренних переменных структуры, размещаемых в памяти друг за другом. Тем самым, структура напрямую трансформировалась бы в массив данных типа float, который мы задействовали бы в качестве буферного массива. Ниже представлен наглядный пример того, как данные такой структуры могут быть расположены в памяти:

Благодаря этому крайне полезному свойству мы можем напрямую передать в качестве данных буфера указатель на большой перечень структур с вершинами, и они прекрасно преобразуются в тот вид аргумента функции glBufferData(), который она от нас ожидает получить:

Естественно, оператор sizeof также может быть применен к структуре для вычисления её размера в байтах. В результате у нас должно получиться 32 байта (8 float * 4 байта).

Еще одна немаловажная особенность применения структур — это использование директивы препроцессора offsetof(s,m), которая принимает в качестве своего первого аргумента структуру, а в качестве второго — имя её внутренней переменной. Макрос возвращает смещение (в байтах) данной переменной относительно начала структуры. Это идеально подходит для определения параметра смещения для функции glVertexAttribPointer():

Теперь смещение определяется с помощью макроса offsetof(), который, в этом случае, задает смещение (в байтах) вектора нормали, равное смещению (в байтах) атрибута нормали, которое состоит из 3-х значений типа float (получается 12 байт).

Использование такой структуры не только придает более аккуратный вид нашему коду, но и позволяет легко её расширять. Если нам потребуется еще один атрибут вершины, то мы сможем добавить его в структуру, не сломав при этом код рендеринга.

Рендеринг

Последняя функция, которую нам нужно определить, чтобы завершить создание класса, — это функция Draw(). Перед рендерингом меша и непосредственным вызовом функции glDrawElements(), мы должны привязать соответствующие текстуры. Однако это может быть несколько затруднительно, поскольку мы с самого начала не знаем, сколько и какого типа текстур имеет меш (если вообще имеет). Итак, как же нам в шейдерах задать сэмплеры и текстурные юниты?

Чтобы решить эту проблему, мы примем определенное соглашение об именовании переменных: каждая диффузная текстура будет называться texture_diffuseN, а каждая зеркальная текстура — texture_specularN, где N — это любое число в диапазоне от 1 до максимально разрешенного значения числа текстурных сэмплеров. Допустим, у нас есть 3 диффузные и 2 зеркальные текстуры для конкретного меша, тогда их текстурные сэмплеры должны называться так:

В соответствии с этим соглашением мы можем определить в шейдерах столько сэмплеров текстур, сколько позволяет OpenGL, и, если меш действительно содержит (так много) текстур, мы будем знать, как они будут называться. В соответствии с этим соглашением мы можем обрабатывать любое количество текстур отдельно взятого меша, и разработчику шейдеров ничего не помешает использовать столько текстур, сколько он захочет, определяя при этом соответствующие сэмплеры.

Примечание: Существует большое множество решений подобных задач, и, если вам не нравится конкретно этот способ, вы можете проявить творческий подход и придумать свой собственный метод.

В результате всех описываемых манипуляций код функции отрисовки принимает следующий вид:

Сначала мы вычисляем N-й компонент для каждого типа текстуры и объединяем его со строкой, содержащей тип текстуры, чтобы получить имя соответствующей uniform-переменной. Затем мы находим соответствующий сэмплер, присваиваем ему значение местоположения текущего активного текстурного юнита, и связываем текстуру. Это также является причиной того, почему в функции Draw() нам нужен шейдер.

Мы также добавили приставку "material." к полученному имени uniform-переменной, потому что наши текстуры хранятся в структуре material (стоит заметить, что в зависимости от реализации, данный момент может отличаться).

Примечание: Обратите внимание, что мы увеличиваем счетчики диффузного и зеркального компонентов в тот момент, когда преобразуем их в строку.

Полный исходный код класса Mesh

Вышеопределенный класс Mesh является абстракцией для многих вещей, которые мы обсуждали на первых уроках. На следующем уроке мы создадим класс модели Model, который будет играть роль контейнера для нескольких mesh-объектов, и реализуем с помощью Assimp интерфейс загрузки моделей.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (10 оценок, среднее: 5,00 из 5)
Загрузка...

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

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