Урок №8. Системы координат в OpenGL

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

  Обновл. 10 Сен 2021  | 

 28340

 ǀ   12 

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

Типы систем координат

OpenGL ожидает, что после каждого запуска вершинного шейдера, все вершины, которые мы хотим сделать видимыми, будут находиться в нормализованных координатах устройства. То есть координаты x, y и z каждой вершины должны находиться в интервале от -1.0 до 1.0; координаты вне этого диапазона не будут видны. То, что мы обычно делаем — это указываем координаты в диапазоне (или пространстве), который мы сами определяем, и в вершинном шейдере преобразуем эти координаты в нормализованные координаты устройства (NDC). Затем NDC передаются в растеризатор, чтобы превратить их в 2D-координаты/пиксели на экране.

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

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

   Локальное пространство (или «Пространство объекта», от англ. «Local space»).

   Мировое пространство (англ. «World space»).

   Пространство окна просмотра (или «Пространство вида», от англ. «View space»).

   Отсеченное пространство (англ. «Clip space»).

   Экранное пространство (англ. «Screen space»).

Всё это — набор разных состояний, через которые, как уже было сказано выше, в процессе преобразований пройдут наши вершины прежде, чем окончательно превратятся во фрагменты.

Вы, вероятно, уже совсем запутались в том, что такое пространство или система координат, поэтому мы сначала расскажем о них более «высокоуровневым» способом, показав общую картину процесса и то, что представляет собой каждое конкретное пространство.

Общая картина


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

Рассмотрим детально:

   Шаг №1: Определение локальных координат объекта. Локальные координаты — это координаты объекта относительно его локальной точки отсчета/начала.

   Шаг №2: Преобразование локальных координат в координаты мирового пространства, которые являются координатами относительно начальной точки некоторого большого (глобального) мира, содержащего все объекты, вместе взятые.

   Шаг №3: Преобразование координат мирового пространства в координаты пространства вида (или «окна просмотра») таким образом, чтобы каждая координата являлась видимой с точки зрения камеры или зрителя.

   Шаг №4: Проецирование координат пространства окна просмотра в отсеченные координаты. Координаты отсеченного пространства обрабатываются в диапазоне от -1.0 до 1.0 и определяют, какие вершины окажутся на экране. Проецирование в координаты отсеченного пространства может добавить перспективу, при условии использования перспективной проекции.

   Шаг №5: Преобразование отсеченных координат в экранные координаты в процессе, который мы называем трансформацией окна просмотра, в результате которого координаты от -1.0 до 1.0 трансформируются в диапазон координат, задаваемый через функцию glViewport(). Затем полученные координаты отправляются в растеризатор, чтобы превратиться во фрагменты.

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

Давайте рассмотрим каждую систему координат более подробно.

Локальное пространство

Локальное пространство — это координатное пространство, которое является локальным для объекта, т.е. это то место, где объект берет свое начало. Представьте себе, что вы создали куб в какой-нибудь программе для моделирования (например, Blender). Начало вашего куба, скорее всего, будет находиться в точке (0,0,0) несмотря на то, что в конце вашей работы куб может оказаться в другом месте. Все последующие созданные вами модели, скорее всего, также будут иметь начальное положение в точке (0,0,0). Таким образом, все вершины вашей модели находятся в локальном пространстве: все они локальны для вашего объекта.

Вершины ящика, который мы использовали на предыдущих уроках, были заданы в виде координат, значения которых лежали на отрезке от -0.5 до 0.5, и при этом точка 0.0 являлась точкой начала отсчета. Это и есть локальные координаты.

Мировое пространство

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

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

Пространство окна просмотра

Пространство окна просмотра (иногда также известное как «пространство камеры» или «пространство поля зрения») — это пространство, обозреваемое с точки зрения камеры. Другими словами, это результат преобразования координат мирового пространства в координаты, которые находятся перед взором пользователя. Обычно этот результат достигается с помощью комбинации трансляций и поворотов для перемещения/поворота сцены так, чтобы некоторые элементы трансформировались в позицию перед камерой. Данные комбинированные преобразования чаще всего хранятся в матрице вида, которая преобразует мировые координаты в координаты пространства окна просмотра. На следующем уроке мы подробно обсудим, как создать такую матрицу для имитации камеры.

Отсеченное пространство

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

Поскольку задание всех видимых координат в диапазоне от -1.0 до 1.0 на самом деле не является интуитивно понятным, то для нашей работы мы указываем собственный набор координат и преобразуем их обратно в NDC, как этого ожидает OpenGL.

Для преобразования вершинных координат из координатного пространства окна просмотра в пространство отсеченных координат необходимо определить так называемую матрицу проекции, которая задает диапазон координат, например, от -1000 до 1000 в каждом измерении. Затем матрица проекции преобразует координаты из данного диапазона в нормализованные координаты устройства (-1.0, 1.0). Все координаты, лежащие вне диапазона (-1000; 1000), не спроецируются в диапазон (-1.0; 1.0) и поэтому будут обрезаны. В рамках диапазона, указанного в матрице проекции, точка с координатами (1250, 500, 750) не будет видна, так как её x-координата находится вне диапазона и, таким образом, преобразуется в координату со значением выше 1.0 в NDC, а значит — отсекается.

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

Если треугольник не поместился целиком в объем отсечения, то его вылезшая часть отсекается. Из-за этого треугольник становится четырехугольником. А OpenGL может работать только с треугольниками, поэтому он «восстанавливает»/разбивает многоугольник до одного или нескольких треугольников.

Данное «окно просмотра», созданное матрицей проекции, называется усеченной пирамидой видимости, и каждая координата, которая оказывается внутри этой пирамиды, будет отображаться на экране пользователя. Полный процесс преобразования координат из заданного диапазона в NDC, которые легко могут быть сопоставлены с координатами двумерного видового пространства, называется проекцией, поскольку матрица проекции проецирует 3D-координаты в легко-отображаемые-на-плоскость нормализованные координаты устройства.

Как только все вершины закончат преобразование в координаты отсеченного пространства, начнет выполняться заключительная операция, называемая перспективным делением, на этапе которой компоненты x, y и z соответствующих радиус-векторов мы делим на однородную w-координату; перспективное деление преобразует 4D-координаты отсеченного пространства в 3D-координаты NDC. Данный шаг выполняется автоматически в конце этапа вершинного шейдера. Именно после этого этапа полученные координаты сопоставляются с координатами экрана (используя настройки функции glViewport()) и превращаются во фрагменты.

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

Ортографическая проекция

Матрица ортографической проекции определяет кубообразную усеченную пирамиду видимости, задающую пространство отсечения, в котором отсекается каждая вершина, лежащая вне прямоугольников, являющихся гранями пирамиды видимости. При создании матрицы ортографической проекции мы задаем ширину, высоту и длину данной пирамиды. Все координаты внутри этой пирамиды после преобразования соответствующей матрицей окажутся в пределах диапазона NDC, а значит — не будут отсечены. Рассматриваемая кубообразная усеченная пирамида чем-то похожа на обычный ящик или контейнер:

Пирамида определяет видимые координаты и имеет такие условные составляющие, как: ширина, высота, ближняя и дальняя плоскости. Любая координата перед ближней плоскостью отсекается, и то же самое относится к координатам за дальней плоскостью. Ортографическая пирамида напрямую отображает все координаты внутри себя на нормализованные координаты устройства без каких-либо особых побочных эффектов, так как она не затрагивает w-компоненту преобразованного вектора; если w-компонента остается равной 1.0, то перспективное деление не изменит координаты.

Для создания матрицы ортографической проекции мы используем встроенную GLM-функцию glm::ortho():

Первые два параметра задают левую и правую координаты пирамиды, а третий и четвертый — нижнюю и верхнюю её части. С помощью описанных 4 точек мы определили размер ближней и дальней плоскостей. Далее 5-й и 6-й параметры определяют расстояния между ближней и дальней плоскостями. Указанная матрица проекции преобразует все координаты (x, y и z) заданного диапазона в нормализованные координаты устройства.

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

Перспективная проекция

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

Как вы можете видеть, из-за перспективы линии на достаточно большом удалении от наблюдателя кажутся сходящимися. Это именно тот эффект, который пытается имитировать перспективная проекция, используя матрицу перспективной проекции. Матрица проекции отображает диапазон, задаваемый усеченной пирамидой, в отсеченное пространство и также манипулирует w-значением каждой координаты вершины таким образом, что чем дальше координата вершины находится от зрителя, тем больше становится w-компонента. Как только координаты преобразуются в координаты отсеченного пространства, то их значения будут лежать в диапазоне от -w до w (всё, что выходит за пределы этого диапазона, отсекается). OpenGL требует, чтобы в выходном результате вершинного шейдера все видимые координаты находились в диапазоне от -1.0 до 1.0, поэтому, как только координаты попадают в усеченное пространство, к ним применяется операция деления перспективы:

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

Матрица перспективной проекции может быть создана в GLM следующим образом:

Функция glm::perspective() снова создает большую усеченную пирамиду, которая определяет видимое пространство, а всё, что находится вне её, не попадет в объем отсеченного пространства и, таким образом, отсекается. Перспективная пирамида может быть визуализирована как усеченная пирамида, ограниченная сверху и снизу прямоугольниками разных размеров. Каждая координата, находящаяся между этими прямоугольниками (внутри пирамиды), будет отображена в точку в отсеченном пространстве. Ниже представлена картинка с изображением перспективной пирамиды:

Первый параметр функции glm::perspective() определяет значение FoV (от англ. «Field of View»), которое обозначает поле зрения и задает размер видового пространства. Для реалистичного вида он обычно устанавливается равным 45 градусов, но если вы хотите получить что-то вроде doom-стиля, то можете установить более высокое значение. Второй параметр задает соотношение сторон, которое вычисляется путем деления ширины видового экрана на его высоту. Третий и четвертый параметры задают расстояния до ближней и дальней плоскостей пирамиды. Обычно мы устанавливаем ближнее расстояние равным значению 0.1, а дальнее — 100.0. Все вершины, лежащие между ближней и дальней плоскостями и внутри отсеченного пространства, будут визуализированы.

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

При использовании ортографической проекции каждая из координат вершины напрямую сопоставляется с координатой отсеченного пространства без какого-либо причудливого деления перспективы; на самом деле, оно все еще выполняется, но без использования w-компоненты (её значение остается равным 1), и, следовательно, не имеет никакого эффекта. Поскольку ортографическая проекция не использует перспективную проекцию, объекты, находящиеся дальше, не кажутся меньше, что создает странный визуальный эффект. По этой причине ортографическая проекция в основном используется для 2D-визуализации и некоторых архитектурных или инженерных приложений, где предпочтительно не иметь вершин, искаженных перспективой. Такие приложения для 3D-моделирования, как Blender, иногда используют ортографическую проекцию, потому что она более точно отображает размеры каждого объекта. Ниже вы увидите сравнение обоих методов проекции в Blender:

Вы можете видеть, что при перспективной проекции удаленные вершины кажутся намного меньше, в то время как в ортогональной проекции каждая вершина имеет одинаковое расстояние до пользователя.

Складываем всё вместе

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

Vclip = Mprojection × Mview × Mmodel × Vlocal

Обратите внимание, что порядок умножения матрицы — обратный (помните, что нам нужно читать умножение матрицы справа налево). Затем каждая полученная вершина должна быть назначена в gl_Position в вершинном шейдере, и OpenGL автоматически выполнит деление перспективы и отсечение.

Примечание: Вы можете задаться вопросом: «А что потом?». Выходные данные вершинного шейдера требуют, чтобы координаты находились в отсеченном пространстве, что мы только что и сделали с помощью матриц преобразования. Затем OpenGL выполняет деление перспективы на координатах отсеченного пространства, чтобы преобразовать их в нормализованные координаты объекта. После этого OpenGL использует параметры из функции glViewPort() для сопоставления нормализованных координат объекта с экранными координатами, где каждая координата соответствует точке на нашем экране (в нашем случае экран имеет разрешение 800×600). Этот процесс называется трансформацией окна просмотра.

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

Переходим в 3D


Теперь, когда мы знаем, как преобразовать 3D-координаты в 2D-координаты, мы можем начать визуализировать реальные 3D-объекты вместо убогой 2D-плоскости, которую мы показывали до сих пор.

Чтобы начать рисовать в 3D-, сначала нужно создать матрицу модели. Матрица модели состоит из трансляций, масштабирования и/или поворотов, которые мы хотели бы применить для преобразования всех вершин объекта в мировое пространство. Давайте немного изменим нашу плоскость, повернув её по оси x так, чтобы она выглядела, будто лежит на полу. Тогда матрица модели будет выглядеть следующим образом:

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

Далее нам нужно создать матрицу вида. Мы хотим немного сдвинуться назад на сцене, чтобы объект стал видимым (когда в мировом пространстве мы находимся в начале координат (0,0,0)). Чтобы передвигаться по сцене, необходимо представить следующее:

   Перемещение камеры назад — это то же самое, что перемещение всей сцены вперед.

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

Поскольку мы хотим двигаться назад, а OpenGL — это правосторонняя система, то мы должны двигаться вдоль положительного направления оси z. А выполнить данное передвижение можно переводя сцену в сторону отрицательного направления оси z. Это создает впечатление, что мы движемся назад.

Правосторонняя система (Правило правой руки)

Условно говоря, OpenGL — это правосторонняя система. Данный факт говорит нам о том, что положительная ось x находится справа от нас, положительная ось y — направлена вверх, а положительная ось z — вперед. Представьте, что экран является центром 3 осей, в т.ч. и положительной оси z, проходящей через экран к нам. Оси рисуются следующим образом:

Чтобы понять, почему правило называется «правилом правой руки», сделайте следующее:

   вытяните правую руку вдоль положительной оси y;

   пусть ваш большой палец указывает вправо;

   пусть ваш указательный палец указывает вверх;

   теперь согните средний палец на 90 градусов относительно вашей ладони.

Если вы всё сделали правильно, то ваш большой палец должен быть направлен в сторону положительной оси x, указательный палец — в сторону положительной оси y, а средний палец — в сторону положительной оси z. Если бы вы сделали это левой рукой, то увидели бы, что ось z перевернута. Это называется левосторонней системой и она, обычно, используется в DirectX. Заметим, что в нормализованных координатах объекта OpenGL фактически использует левостороннюю систему (матрица проекции выступает в роли переключателя стрелки).

Переходим в 3D (продолжение)

Более подробно о том, как передвигаться по сцене, мы поговорим на следующем уроке. На данный момент матрица вида имеет следующее представление:

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

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

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

Теперь, когда наши координаты вершин преобразуются через матрицы модели, вида и проекции, конечный объект должен быть:

   лежащим на полу и наклоненным в сторону от нас;

   немного дальше относительно нас;

   иметь перспективу (он должен становиться меньше, чем дальше находятся его вершины).

Давайте проверим, действительно ли результат соответствует этим требованиям:

Это действительно выглядит так, как будто наша плоскость — это трехмерная плоскость, которая лежит на каком-то воображаемом полу. Если вы не получили вышеприведенный результат, то сравните свой код с полным исходным кодом, ссылка на который приведена ниже.

  GitHub / Урок №8. Системы координат в OpenGL — Исходный код №1

Еще больше 3D

До сих пор даже в 3D-пространстве мы работали с 2D-плоскостью, поэтому давайте расширим нашу 2D-плоскость до 3D-куба. Для визуализации куба нам нужно в общей сложности 36 вершин (6 граней * 2 треугольника * 3 вершины каждый). 36 вершин — это очень много, чтобы суммировать их, так что проще взять их отсюда:

Для развлечения мы позволим кубу вращаться с течением времени:

А затем мы отобразим куб с помощью функции glDrawArrays() (так как мы не указывали индексы), но на этот раз с количеством вершин, равным 36:

Мы должны получить что-то похожее на следующее:

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

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

  GitHub / Урок №8. Системы координат в OpenGL — Исходный код №2

Z-буфер


OpenGL хранит всю свою информацию о глубине в z-буфере, также известном как буфер глубины. GLFW автоматически создает такой буфер для нас (точно так же, как это было и с буфером цвета, который хранит цвета выходного изображения). Информация о глубине хранится в рамках каждого фрагмента (как z-значение фрагмента), и всякий раз, когда фрагмент хочет вывести свой цвет, OpenGL сравнивает его значения глубины с z-буфером. Если текущий фрагмент находится позади другого фрагмента, то он отбрасывается, а в противном случае — перезаписывается. Этот процесс называется тестированием глубины и выполняется автоматически средствами OpenGL.

Однако, если мы хотим убедиться, что OpenGL действительно выполняет тестирование глубины, нам сначала нужно сообщить OpenGL, что мы хотим включить глубинное тестирование; ведь по умолчанию оно отключено. Мы можем включить глубинное тестирование с помощью функции glEnable(). Функции glEnable() и glDisable() позволяют нам включать/отключать определенный функционал в OpenGL до тех пор, пока не будет сделан другой вызов для его включения/отключения. Прямо сейчас мы хотим включить глубинное тестирование, задействовав GL_DEPTH_TEST:

Поскольку мы используем буфер глубины, то нам также нужно его очищать перед каждой итерацией рендеринга (в противном случае информация о глубине предыдущего кадра останется в буфере). Точно так же, как и с очисткой цветового буфера, мы можем очистить буфер глубины, указав бит DEPTH_BUFFER_BIT в функции glClear():

Давайте снова запустим нашу программу и посмотрим, выполняет ли теперь OpenGL глубинное тестирование:

Ну вот и всё! Полностью текстурированный куб с правильным тестированием глубины, который вращается с течением времени.

  GitHub / Урок №8. Системы координат в OpenGL — Исходный код №3

Больше кубиков!

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

Для этого сначала определим массив типа glm::vec3, состоящий из 10-ти векторов трансляции, каждый из которых будет задавать для соответствующего куба его положение в мировом пространстве:

Далее в цикле рендеринга нам нужно 10 раз (по количеству кубиков) вызвать функцию glDrawArrays(), но при этом перед каждым обращением к данной функции мы будем посылать в вершинный шейдер новую матрицу модели. Создадим небольшой цикл внутри цикла рендеринга, который на каждом шаге основного цикла будет рендерить наш объект 10 раз, но применяя обновленную матрицу модели. Обратите внимание, что мы также добавляем небольшое уникальное вращение к каждому контейнеру:

Этот фрагмент кода будет обновлять матрицу модели каждый раз, когда рисуется новый куб, и делать это он будет в общей сложности 10 раз. Прямо сейчас мы должны увидеть мир, наполненный 10-ю странно вращающимися кубиками:

Прекрасно! Похоже, наш контейнер нашел себе единомышленников. Если вы застряли, смотрите исходный код, ссылка на который приведена ниже.

  GitHub / Урок №8. Системы координат в OpenGL — Исходный код №4

Упражнения

Задание №1

Попробуйте поэкспериментировать с параметрами FoV и соотношением сторон GLM-функции проекции. Посмотрите, сможете ли вы понять, как они влияют на пирамиду перспективы.

Задание №2

Поиграйтесь с матрицей вида, выполняя трансляцию в нескольких направлениях, и посмотрите, как меняется сцена. Подумайте о матрице вида как об объекте камеры.

Задание №3

Попробуйте заставить каждый 3-й контейнер (включая 1-й) вращаться с течением времени, оставляя другие контейнеры статичными, используя только матрицу модели.

Ответ №3

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

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

Комментариев: 12

  1. Вася:

    Статья очень хорошо помогает новичкам понять то как примерно работают матрицы в трансформациях и как использовать их в коде.
    Однако сами трансформации тема глубокая, а эта статья довольно поверхностно объясняет их. Тут рассмотрена только матрица MVP (model, view, projection), но про построение матрицы модели, и что очень важно, манипуляции матрицами вращения сказано очень мало. После прочтения данной статьи лично я около месяца не был способен заставить условный самолет вращаться как надо. Благодаря этой проблеме я изучил особенности матрицы lookAt и почему он не применим к объектам, и узнал про кватернионы, которые помогают совершать все необходимые виды вращения.
    Но спасибо огромное за статью, все объяснено максимально просто, без нее я бы даже не знал с чего начать

  2. S0ulM1ke:

    Скажите пожалуйста, в последнем примере кубики должны крутиться?
    Потому что у меня не крутятся. Я уже и код полностью скопировал вместе с шейдерами. Все равно стоят на месте. Я просто не понимаю в какой части кода они должны "крутиться". В цикле angle мы присваиваем одно и тоже значение каждый раз как заходим туда. Не понимаю(

    1. Kazmi:

  3. Иван:

    В последнем примере первый ящик не крутится, потому что его угол поворота на первой итерации цикла умножается на 0:

    т.к. переменная i в начале равна нулю, нужно её хотя бы на 1 увеличить

  4. DaniilChik:

    Здравствуйте! Не могли бы подсказать, сферу в opengl тоже можно отобразить с помощью треугольников, и если да — то как?

    1. DaniilChik:

      У меня получилось создать подобие сферы вот таким куском кода:

      Но, к сожалению он opengl рисует это все как штатив с увеличивающимися по радиусу окружностями при переходе к центру (как я понял, соединение в треугольники происходит неправильно в моем случае)

  5. Grave18:

    Интересно, что с view матрицей нужно сначала применить перемещение, а потом вращение, чтобы был эффект вращения мира вокруг камеры.

    1. Вася:

      Результат умножения матриц зависит оттого в каком порядке они умножены (т.е. A*B != B*A)

  6. Trigun:

    Здравствуйте! Немного непонятно в последнем примере Урок 8 исходный код 4 на гитхабе. Если мы отрисовываем например два объекта, зачем до их отрисовки опредилять матрицу модели:

    Строки 223 и 226.
    Насколько я понял матрица модели применяестя к объекту и если мы например захотим просто нарисовать два объеста то опредилим два раза матрицу модели применив к ним нужные нам трансформации и затем передадим два раза матрицу модели в шейдер.
    Зачем по сути передавать в шейдер матрицу модели несуществующего объекта который не будет отрисован как в строках 223 и 226? Или я что то не так понял?

    1. Фото аватара Дмитрий Бушуев:

      >>Зачем по сути передавать в шейдер матрицу модели несуществующего объекта который не будет отрисован как в строках 223 и 226? Или я что то не так понял?

      Добрый день.
      Вы поняли всё правильно. Эти действия — лишние, и случайно попали в код данного примера.

      P.S: Необходимые изменения уже внесены в github-репозиторий. Спасибо за подсказку 🙂

      1. Сергей Федоров:

        Не совсем лишние. Ящики тогда статичные. Рандом на каждом цикле рендеринга где-то нужен всё равно.
        Например изменить строку 238:

  7. veterok232:

    Здравствуйте! Спасибо за ваши уроки! У меня появилась такая проблема. При если запускать программу на интегрированной графике(Intel UHD Graphics), то все отображается без проблем, но если запустить на дискретной видеокарте Nvidia Geforce MX130, то появляются проблемы с отображением текстур(корректно отображается только один из треугольников, составляющих прямоугольник). Драйвера стоят новые. Вы не знаете в чем может быть проблема?

Добавить комментарий для Вася Отменить ответ

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