Каковы некоторые рекомендации для кодирования OpenGL (особенно в отношении объектной ориентации)?

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

Я пришел из объектно-ориентированного фона, поэтому я пытаюсь поместить все, что мы делаем, в повторно используемые классы. У меня был хороший успех в создании класса камеры, так как он зависит в основном от одного вызова gluLookAt(), который в значительной степени не зависит от остальной части конечного автомата OpenGL.

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

Я начинаю задаваться вопросом, применимы ли принципы ОО в кодировании OpenGL. По крайней мере, я думаю, что я должен сделать свои занятия менее детальными.

Каково мнение сообщества по переполнению стека на это? Каковы ваши лучшие практики для кодирования OpenGL?

5 ответов

Решение

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

ООП или нет, для рендеринга какой-либо сцены это различные типы и сущности, которые у вас обычно есть:

Геометрия (сетки). Чаще всего это массив вершин и массив индексов (т.е. три индекса на треугольник, или "список треугольников"). Вершина может быть в произвольном формате (например, только позиция float3; позиция float3 + нормаль float3; позиция float3 + нормаль float3 + текстовая координата float2; и т. Д. И т. Д.). Итак, чтобы определить кусок геометрии вам нужно:

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

Если вы находитесь на земле ООП, вы можете назвать этот класс сеткой.

Материалы - вещи, которые определяют способ визуализации некоторого фрагмента геометрии. В простейшем случае это может быть, например, цвет объекта. Или следует ли применять освещение. Или должен ли объект быть альфа-смешанным. Или текстуру (или список текстур) для использования. Или вершинный / фрагментный шейдер для использования. И так далее, возможности безграничны. Начните с того, что положите в материалы то, что вам нужно. В ООП земле этот класс можно назвать (сюрприз!) Материалом.

Сцена - у вас есть кусочки геометрии, коллекция материалов, время, чтобы определить, что находится на сцене. В простом случае каждый объект в сцене может быть определен с помощью: - какой геометрии он использует (указатель на сетку), - как он должен отображаться (указатель на материал), - где он расположен. Это может быть матрица преобразования 4x4 или матрица преобразования 4x3, или вектор (положение), кватернион (ориентация) и другой вектор (масштаб). Давайте назовем это Узлом в ООП земле.

Камера. Ну, камера - это не что иное, как "где она находится" (опять же, матрица 4x4 или 4x3 или положение и ориентация), плюс некоторые параметры проекции (поле зрения, соотношение сторон, ...).

Так что в основном это все! У вас есть сцена, представляющая собой набор узлов, которые ссылаются на сетки и материалы, и у вас есть камера, которая определяет, где находится зритель.

Теперь, где разместить реальные вызовы OpenGL - это вопрос дизайна. Я бы сказал, не помещайте вызовы OpenGL в классы Node, Mesh или Material. Вместо этого создайте что-то вроде OpenGLRenderer, которое может пройти через сцену и выполнить все вызовы. Или, что еще лучше, создайте что-то, что проходит через сцену независимо от OpenGL, и поместите вызовы более низкого уровня в зависимый класс OpenGL.

Так что да, все вышеперечисленное в значительной степени не зависит от платформы. Пройдя по этому пути, вы обнаружите, что glRotate, glTranslate, gluLookAt и друзья совершенно бесполезны. У вас уже есть все матрицы, просто передайте их OpenGL. Вот как все-таки работает большая часть реального кода в реальных играх / приложениях.

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

Но основная идея проста: есть Геометрия, есть Материалы, есть объекты на сцене. Затем какой-то небольшой фрагмент кода может их визуализировать.

В случае OpenGL установка сеток, скорее всего, создаст / активирует / изменит объекты VBO. Перед тем, как какой-либо узел будет визуализирован, необходимо установить матрицы. И настройка Material коснется большей части оставшегося состояния OpenGL (смешивание, текстурирование, освещение, объединители, шейдеры, ...).

Трансформации объектов

Избегайте зависимости от OpenGL для ваших преобразований. Часто учебники научат вас, как играть со стеком матриц преобразования. Я бы не рекомендовал использовать этот подход, так как позже вам может понадобиться некоторая матрица, которая будет доступна только через этот стек, и использовать ее очень долго, поскольку шина GPU рассчитана на быструю передачу от CPU к GPU, а не наоборот.

Мастер объект

Трехмерная сцена часто рассматривается как дерево объектов, чтобы знать зависимости объектов. Существует дискуссия о том, что должно быть в корне этого дерева, списка объектов или главного объекта.

Я советую использовать мастер-объект. Хотя он не имеет графического представления, он будет проще, потому что вы сможете использовать рекурсию более эффективно.

Отделите сцену менеджера и рендерера

Я не согласен с @ejac, что у вас должен быть метод для каждого объекта, выполняющего вызовы OpenGL. Отдельный класс Renderer, просматривающий вашу сцену и выполняющий все вызовы OpenGL, поможет вам отделить логику сцены и код OpenGL.

Это добавляет некоторые сложности при проектировании, но даст вам больше гибкости, если вам когда-нибудь придется перейти с OpenGL на DirectX или что-нибудь еще, связанное с API.

Стандартный метод состоит в том, чтобы изолировать влияние объектов на состояние рендеринга друг от друга, выполнив все изменения из некоторого состояния OpenGL по умолчанию в области glPushAttrib/glPopAttrib. В C++ определяют класс с конструктором, содержащий

  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);

и деструктор, содержащий

  glPopClientAttrib();
  glPopAttrib();

и использовать класс RAII-style, чтобы обернуть любой код, который мешает состоянию OpenGL. При условии, что вы следуете шаблону, каждый метод рендеринга объекта получает "чистый лист" и ему не нужно беспокоиться о том, чтобы подтолкнуть каждый возможный измененный бит состояния openGL к тому, что ему нужно.

В качестве оптимизации, как правило, вы устанавливаете состояние OpenGL один раз при запуске приложения в какое-то состояние, максимально приближенное к желаемому; это сводит к минимуму количество вызовов, которые должны быть сделаны в заданных областях.

Плохая новость - это не дешевые звонки. Я никогда не исследовал, сколько в секунду вы можете сойти с рук; конечно, достаточно, чтобы быть полезным в сложных сценах. Главное - попытаться максимально использовать состояния, как только вы их установите. Если у вас есть армия орков для рендеринга с различными шейдерами, текстурами и т. Д. Для доспехов и скинов, не перебирайте все орки, рендеринг armor/skin/armor/skin/...; убедитесь, что вы установили состояние для доспехов один раз и отрендерили доспехи всех орков, а затем настроили рендеринг всего скина.

Если вы хотите сделать свой собственный ответ, вышеприведенные ответы сработают достаточно хорошо. Многие из упомянутых принципов реализованы в большинстве графических движков с открытым исходным кодом. Сценографы - это один из способов отойти от рисования OpenGL в прямом режиме.

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

У меня обычно есть функция drawOpenGl() для каждого класса, который может быть визуализирован, который содержит вызовы opengl. Эта функция вызывается из renderloop. Класс содержит всю информацию, необходимую для его вызовов функции opengl, например. о положении и ориентации, чтобы он мог сделать свое собственное преобразование.

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

Это было некоторое время, но я думаю, что нечто подобное возможно с текстурами.

Если вы хотите переключаться между нормалью поверхности или вершинной нормалью, то пусть объект запоминает, является ли он той или другой, и имеет 2 приватных функции для каждого случая, которые drawOpenGL() вызывает при необходимости. Конечно, есть и другие более элегантные решения (например, использование шаблона разработки стратегии или чего-то еще), но это может работать, насколько я понимаю вашу проблему

Другие вопросы по тегам