Урок №17. Mesh в OpenGL

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

  Обновл. 14 Авг 2020  | 

 1100

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

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

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

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

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

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

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

Как вы видите, наш класс довольно простой. Для создания mesh-объекта мы передаем конструктору класса все необходимые данные, затем инициализируем буферы в функции setupMesh() и, в конце, отрисовываем mesh с помощью функции 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(). Перед рендерингом mesh-а и непосредственным вызовом функции glDrawElements(), мы должны привязать соответствующие текстуры. Однако это может быть несколько затруднительно, поскольку мы с самого начала не знаем, сколько и какого типа текстур имеет mesh (если вообще имеет). Итак, как же нам в шейдерах задать сэмплеры и текстурные юниты?

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

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

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

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

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

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

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

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

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


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

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

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

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