learnopengl. Урок 1.8 — Системы координат

imageВ предыдущем уроке мы узнали о том, какую пользу можно получить от преобразования вершин матрицами трансформаций. OpenGL предполагает, что все вершины, которые мы хотим увидеть, после запуска шейдера будут в нормализованных координатах устройства (NDC — normalized device coordinates). Это означает, что x, y и z координаты каждой вершины должны быть между -1.0 и 1.0; координаты вне этого диапазона видны не будут. Обычно мы указываем координаты в диапазоне, который настраиваем самостоятельно, а в вершинном шейдере преобразовываем эти координаты в NDC. Затем, эти NDC передаются растеризатору для преобразования их в двумерные координаты/пикселы вашего экрана.


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


  1. Локальное пространство (или пространство Объекта)
  2. Мировое пространство
  3. пространство Вида (или Наблюдателя)
  4. пространство Отсечения
  5. Экранное пространство

Наши вершины будут преобразованы во все эти различные состояния, прежде чем превратятся во фрагменты.


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


Общая схема


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


image

  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 (иногда оно также называется пространство камеры или наблюдателя). Пространство вида это результат преобразования мировых координат в координаты, которые выглядят, как будто пользователь смотрит на них спереди. Таким образом пространство вида — это пространство видимое через видоискатель камеры. Это обычно достигается совокупностью таких сдвигов и вращений сцены, что некоторые объекты располагаются перед камерой. Эти комбинированные преобразования как правило хранятся в матрице вида, которая трансформирует мировые координаты в пространство вида. В следующем уроке мы широко обсудим, как создавать такую матрицу вида, для имитации камеры.


Пространство Отсечения


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


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


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

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

Этот объем просмотра, задаваемый матрицей проекции, называется усечённой пирамидой (frustum) и каждая координата попадающая в эту пирамиду окажется и на экране пользователя. Весь процесс конвертации координат определенного диапазона в нормализованные координаты устройства (NDC), которые могут с легкостью отображены в двумерные координаты пространства вида, называется проецирование, так как матрица проекции проецирует 3D координаты на простые-для-преобразования-в-2D нормализованные координаты устройства.


Как только координаты всех вершин будут переведены в пространство отсечения, выполняется заключительная операция, называемая перспективное деление. В ней мы делим x,y и z компоненты вектора позиции вершины на гомогенную компоненту вектора w. Перспективное деление преобразует 4D координаты пространства отсечения в трехмерные нормализованные координаты устройства. Этот шаг выполняется автоматически после завершения работы каждого вершинного шейдера.


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


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


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


Матрица ортографической проекции задает усечённую пирамиду в виде параллелограмма, который является пространством отсечения, где все вершины, находящиеся вне его объема отсекаются. При создании матрицы ортографической проекции мы задаем ширину, высоту и длину видимой пирамиды отсечения. Все координаты, которые после их преобразования матрицей проекции в пространство отсечения попадают в ограниченный пирамидой объем отсечены не будут. Усеченная пирамида выглядит немного похожей на контейнер:



Усеченная пирамида ортографической проекции

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


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


glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f );

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


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


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


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


Перспектива

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


Формула перспективного деления


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


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


glm::mat4 proj = glm::perspective( 45.0f, (float)width/(float)height, 0.1f, 100.0f);

glm::perspective создает усеченную пирамиду, которая определяет видимое пространство, а все, что находится за его пределами и не попадет в объем пространства отсечения будет обрезано. Перспективная усеченная пирамида может быть представлена как коробка трапециевидной формы, каждая координата внутри которой будет отображена в точку в пространстве отсечения. Изображение перспективной усеченной пирамиды показано ниже:


Перспективная усеченная пирамида

Первый параметр устанавливает значение fov (field of view), что обозначает "поле обзора", и определяет, насколько велика видимая область. Для реалистичного представления этот параметр обычно устанавливается равным 45.0f, но для получения подобия doom-стилю вы можете задавать и большие значения. Второй параметр задает соотношение сторон, которое рассчитывается путем деления ширины области просмотра на её высоту. Третий и четвертый параметры задают ближнюю и дальнюю плоскости усеченной пирамиды. Обычно мы устанавливаем ближайшее расстояние равным 0.1f, а дальнее 100.0f. Все вершины расположенные между ближней и дальней плоскостью и попадающие в объем усеченной пирамиды будут визуализированы.


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

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


Сравнение проекций


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


Собираем все вместе


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


Формула координат отсечения


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


Что потом?
Координаты выходных данных вершинного шейдера должны находиться в пространстве отсечения, чего мы только что и добились с помощью матриц преобразования. OpenGL выполняет перспективное деление координат пространства отсечения, чтобы преобразовать их в нормализованные координаты устройства
Затем OpenGL использует параметры из glViewPort для сопоставления нормализованных координат устройства экранными координатами, в которых каждая координата соответствует точке на вашем экране (в нашем случае это область 800x600). Этот процесс называется преобразованием области просмотра.

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

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


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


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


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


glm::mat4 model;
model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f);

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


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


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

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


Правая система координат

По соглашению, OpenGL является правой координатной системой. В основном это говорит о том, что положительная ось X направлена в право от вас, положительная ось Y вверх, а положительная ось Z на вас (т.е назад). Представьте, что ваш экран является центром трех осей, и положительная ось Z проходит через экран по направлению к вам. Оси изображаются таким образом:


Оси правой системы координат


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

  • Протяните правую руку вдоль положительной оси Y вверх.
  • Пусть ваш большой палец указывает вправо.
  • Пусть указательный палец будет направлен вверх.
  • Теперь согните средний палец на 90 градусов.

Если вы все сделали правильно, то ваш большой палец должен указывать направление положительной оси X, указательный палец — положительную ось Y, а средний палец на положительную ось z. Если вы проделаете то же самое левой рукой, то увидите, что ось Z изменит направление. Такая система координат известна как левая и обычно используется в DirectX. Обратите внимание, что в нормализованных координатах устройства OpenGL фактически использует левую систему (матрица проекции переключает направление).


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


glm::mat4 view;
// Обратите внимание, что мы смещаем сцену в направлении обратном тому, в котором мы хотим переместиться
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

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


glm::mat4 projection;
projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f);

Будьте внимательны при указании градусов в GLM. Здесь мы устанавливаем параметр fov равным 45 градусам, но некоторые реализации GLM принимают fov в радианах, в этом случае вам нужно задать его как glm::radians(45.0).

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


#version 330 core
layout (location = 0) in vec3 position;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // Заметьте, что мы читаем умножение справа налево
    gl_Position = projection * view * model * vec4(position, 1.0f);
    ...
}

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


GLint modelLoc = glGetUniformLocation(ourShader.Program, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
... // Так же для матриц Вида и Проекции

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


  • Отклонен назад до самого пола.
  • Немного от нас удалён.
  • Быть отображенным с учетом перспективы (его размеры должна становиться меньше с увеличением расстояния до зрителя).

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


Вид объекта в перспективе


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


Больше 3D


До сих пор мы работали с 2D-плоскостью, но в 3D-пространстве, поэтому давайте совершим небольшое приключение и расширим нашу 2D-плоскость до 3D-куба.

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


Удаление из массива вершин атрибутов цвета меняет размер «шага» между вершинами, поэтому необходимо исправить этот параметр в вызовах функции glVertexAttribPointer:


// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
...
// TexCoord attribute
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));

Для разнообразия зададим кубу вращение:


model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f));

И теперь нарисуем куб используя glDrawArrays, но на этот раз с количеством 36 вершин.


glDrawArrays(GL_TRIANGLES, 0, 36);

Вы должны получить что-то похожее на это:




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


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


Z-буфер


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


Тем не менее, если мы хотим быть уверены в том, что OpenGL действительно выполняет проверку глубины, то сначала нам нужно её включить, потому что по-умолчанию она выключена. Включение проверки глубины осуществляется с помощью функции glEnable. Функции glEnable и glDisable позволяют включить/отключить определенные возможности OpenGL. Опции OpenGL включаются/выключаются, пока не будет сделан другой вызов функции для их отключения/включения. На данный момент мы хотим разрешить проверку глубины, включая параметр GL_DEPTH_TEST:


glEnable(GL_DEPTH_TEST);

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


glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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



Вот и всё! Наш куб полностью текстурирован, с правильной проверкой глубины, еще и вращается. Здесь исходный код для проверки.


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


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


Во-первых, давайте определим для каждого куба вектор перемещения, который задаст положение объекта в мировом пространстве. Мы запишем 10 позиций кубов в массиве glm::vec3:


glm::vec3 cubePositions[] = {
  glm::vec3( 0.0f,  0.0f,  0.0f), 
  glm::vec3( 2.0f,  5.0f, -15.0f), 
  glm::vec3(-1.5f, -2.2f, -2.5f),  
  glm::vec3(-3.8f, -2.0f, -12.3f),  
  glm::vec3( 2.4f, -0.4f, -3.5f),  
  glm::vec3(-1.7f,  3.0f, -7.5f),  
  glm::vec3( 1.3f, -2.0f, -2.5f),  
  glm::vec3( 1.5f,  2.0f, -2.5f), 
  glm::vec3( 1.5f,  0.2f, -1.5f), 
  glm::vec3(-1.3f,  1.0f, -1.5f)  
};

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


glBindVertexArray(VAO);
for(GLuint i = 0; i < 10; i++)
{
  glm::mat4 model;
  model = glm::translate(model, cubePositions[i]);
  GLfloat angle = 20.0f * i; 
  model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
  glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

  glDrawArrays(GL_TRIANGLES, 0, 36);
}
glBindVertexArray(0);

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


Множество текстурированных кубов


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


Упражнения


  • Создавая матрицу проекции, попробуйте поэкспериментировать в функции GLM projection с параметрами FOV и соотношением сторон.
    Посмотрите, сможете ли вы разобраться, как эти параметры влияют на перспективную усеченную пирамиду.
  • Поиграйте с матрицей вида, сдвигая координаты в различных направлениях и посмотрите, как изменится сцена. Подумайте о матрице вида как о камере.
  • Попробуйте сделать вращающимся каждый 3-й контейнер (в том числе 1), а остальные оставить неподвижными, используя при этом только матрицу модели:
    решение.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 17
  • 0
    Скажите, пожалуйста, как можно победить Z-fighting без удаления near-clip plane на злостное расстояние?
    • +1
      О Z-fighting будет идти речь в уроке, посвященном проверке глубины (Depth testing'у). То есть в первом уроке чтвёртого раздела (Advanced-OpenGL/Depth-testing). Что бы до него добраться, нужно перевести еще 10 уроков.
      Но, к сожалению, и в нем информации не много, всего три совета:
      1) Не допускать «перекрывания» объектов
      2) По возможности устанавливать ближнюю плоскость отсечения максимально далеко
      3) Если позволяет производительность, использовать повышенную (32bit) точность буфера глубины
      • 0
        Для авиасима — бороться с мерцанием озера попеременно с ландшафтом — и показывать кабину тут же, не далее 20 сантиметров. Пока не могу понять как это «ближнюю плоскость максимально далеко» уносить. Разве что масштабировать кабину на 10 метров диаметра, чтобы крыло напарника в кабину влезало.
        • 0

          Вы можете рисовать кабину отдельным проходом. Например, нарисовать весь мир (допустим, с камерой от 5м до 10км), потом очистить буфер глубины и спокойно нарисовать кабину поверх. (уже, например, с камерой 5м-10см).


          Хотя в идеале было бы лучше сначала нарисовать кабину, чтобы для ландшафта зазря не считать цвета пикселей, которые будут закрыты кабиной.

      • +3
        Нужно использовать обратный Z-Buffer (Reversed-Z)

        https://nlguillemot.wordpress.com/2016/12/07/reversed-z-in-opengl/
        • 0
          Любопытная идея, спасибо!
          Для, например, авиасимулятора не возникнет ли обратной проблемы?
          Вот, развернули Z-Buffer, озеро перестало мерцающе бороться с ландшафтом — не начнут ли теперь за глубину бороться близ летящие самолётики?
          • 0
            Нет, не начнут.
            Распределение float не линейное (как и Z-buffer). Особенность подхода в том, что экспонента float-а растёт соответственно направлению Z. Поэтому высокая точность достигается в любой точке Z-буфера. Естественно это работает для диапазона [0,1] (для GL необходимо использование glClipControl)

            https://developer.nvidia.com/content/depth-precision-visualized

            Что касается производительности float Z-buffer, то она не хуже int даже на древних картах. А в AMD 24-битной глубины нет вообще.
            • 0
              А в AMD 24-битной глубины нет вообще.

              Извините что? Там все как у людей, 24-депз 8-стенцил всю жизнь было. А через фбо можно депз и на все 32 сделать, в том числе сразу во флоат
        • 0
          GL_POLYGON_OFFSET_FILL вам в помощь)))
          • 0

            В случаях с рисованием декалей может помочь glPolygonOffset


            Он чуть-чуть сдвигает глубину. Например, вы рисуете стенку, а поверх неё — след от пули. С помощью оффсета можно сделать, чтобы находящийся прямо на стенке след, гарантированно рисовался "поверх".


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

            • 0
              Да, заметил.
              Читал, что некто где-то рекомендовал даже озёра рисовать с офсетом, но тогда они должны быть видны через горы…
          • +2
            А почему автор сменился? Ну, то есть, это ни хорошо ни плохо, просто любопытно.
            • 0
              На самом деле это уже не первый раз: урок 1.5 тоже был опубликован «новичком» (@FERusM).
            • +2

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

              • 0
                Это же перевод, просто указать забыли.
                • +1
                  Спасибо за комплименты. Полностью согласен, автор оригинала (Joey de Vries) их заслужил.

                  Да, начинающим всё это понять не просто, но благодаря иллюстрациям и сопутствующим примерам, вполне по силам. Не думаю, что объемная теоретическая часть уроков — это лишнее. Бывают, конечно, самоучители в стиле Lazy Foo, где почти голый код. Там точно «лишнего» нет совсем, но вот усвояемость информации, как мне кажется, от этого жутко хромает.

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

                  P.S.: написать (вернее упомянуть) о буфере глубины автор был вынужден для полноценной демонстрации примера куба.
                  • 0

                    А я бы спецом оставил куб с косяками, сразу стало бы видно, внимательно ли народ читал статью )

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