Что такое вершинный буфер? Как создать трёхмерный объект и отрисовать его на экран? Для чего нужен формат вершин и как с ним работает вертексный шейдер? Как работает буфер глубины и что такое борьба за глубину? Как это влияет на полупрозрачность и почему важен порядок отрисовки объектов на экран? Как посчитать координаты камеры и задать перспективу? Для чего нужны матрицы и как ими пользоваться? Что такое отсечение и зачем оно нужно?
В первой части я показал как отрисовать куб и начать его вращать.
Если нужно вращать куб вокруг другой точки, например вершины G, сначала смещаем его так, чтобы эта точка оказалась в начале координат, а затем выполняем вращение. Для этого создаю матрицу смещения и умножаю её на матрицу вращения.
В строке, обозначенной решёткой, начинается событие Draw. GMEdit показывает все события объекта в одном листинге и разделяет их таким образом. В строках 49-50 видно, что я включаю работу с буфером глубины. В четвёртой строке я создаю матрицу смещения, чтобы вершина G оказалась в начале координат. В шестой строке перемножаю матрицу смещения `ma` и матрицу вращения `mt`.
Теперь куб вращается вокруг вершины G. Порядок перемножения матриц имеет значение. Например, у меня есть матрица перемещения T и матрица вращения R . Если я перемножу их как T x R , то сначала куб переместится, а потом будет вращаться вокруг начала координат. Если же перемножу как R x T , то сначала куб будет вращаться, а затем переместится.
Или другой пример. Допустим, у меня есть матрица куба M и матрица R для вращения вокруг оси Y. Если перемножить их как R x M , куб будет вращаться относительно своей локальной оси Y, а если как M x R , он будет вращаться относительно глобальной оси Y, как показано на гифке ниже, где ось Y обозначена зелёной линией, а ось X — красной.
Вращение камеры и перспектива
Я остановил вращение куба и его смещение. Теперь он снова отрисовывается в начале координат. Далее я изменю параметры камеры, переместив её в мире и задав перспективную проекцию вместо ортогональной. Для этого необходимо создать соответствующие матрицы и применить их к текущей камере.
В третьей строке я получаю текущую камеру. В четвёртой строке строю матрицу вида. Функция matrix_build_lookat принимает девять параметров: позицию камеры, позицию цели камеры и направление оси, указывающей вверх. Наш куб находится в начале координат, поэтому в качестве цели я указываю начало координат. В пятой строке создаётся матрица перспективы. Функция matrix_build_projection_perspective_fov принимает 4 параметра: угол обзора, соотношение сторон, расстояние до ближнего и дальнего сечений. По умолчанию GMS2 создаёт комнату с разрешением 1366x768, поэтому я указываю соответствующие значения для соотношения сторон. Параметры ближнего и дальнего отсечения задают диапазон глубины, в пределах которого пиксели будут отображаться на экране. В строках 7 и 8 полученные матрицы устанавливаются для текущей камеры, а в девятой строке я активирую эти значения для текущего шага, иначе они вступят в силу только в следующем.
Теперь куб отрисован в перспективе.
По нажатию кнопки мыши я буду вращать камеру вокруг куба. Для этого потребуется знать радиус, широту и долготу.
В строках 3-5 при нажатии кнопки мыши я изменяю значения широты и долготы, используя смещение мыши за один шаг. В строках 7-10 рассчитываю координаты камеры, а в строке 13 устанавливаю их.
Вот результат:
Тут я хотел закончить гайд, но решил что нужно подробнее рассказать об отсечение? полупрозрачности и упомянуть про борьбу за глубину.
Когда 2 поверхности расположены близко друг к другу, то отрисовываясь на экран некоторые фрагменты задней поверхности могут отрисовываться спереди, как это видно на гифке внизу.
Это происходит потому что точности буфера глубины недостаточно чтобы правильно расположить пиксели по глубине и чем дальше такие поверхности будут от камеры, тем больше будет погрешность.
Чтобы показать как работает отсечение я уберу верхнюю грань у куба.
Видно нижнюю грань и заднюю. Это потому что сейчас отсечение выключено и по этому треугольники отрисовываются на экран независимо от порядка вершин по или против часовой стрелки. Теперь с помощью функции gpu_set_cullmode я включаю отсечение.
На этой гифке видно что теперь грани которые отвёрнуты от камеры не отрисовываются, благодаря включённому отсечению.
Теперь я сделаю одну из плоскотей полупрозрачной для теста. Через неё должно быть видно бэкграунд и заднюю тёмносерую плоскость.
На этой гифке видно, что через полупрозрачную плоскость не видно заднюю тёмносерую плоскость. Это происходит потому что полупрозрачная плоскость отрисовывается первой и буфер глубины забивается её значениями, по этому когда я отрисовываю заднюю плоскость, то её не видно, потому что из-за занятого в этом месте буфера глубины отрисовка останавливается. Теперь я поменяю порядок отрисовки так чтобы полупрозрачная плоскость отрисовывалась позже чем задняя тёмносерая и теперь на гифке внизу видно что проблема была исправлена.
Иногда разработчики игр вместо полупрозрачности используют дизеринг. Благодаря этому можно не беспокоится о порядке отрисовки объектов на экран.
Итак, в этом гайде я рассказал, как создать вершинный буфер и отрисовать его. Рассмотрел работу с матрицами, вращение куба со смещением и вращение вокруг разных осей. Также разобрал, как рассчитать положение камеры в пространстве и задать перспективу. Я объяснил, что такое буфер глубины, борьба за глубину и полупрозрачность. Упомянул про отсечение и немного рассказал о GMEdit.
Если вам понравилась эта статья, то:
Можно ознакомиться с первой частью
Статьёй о шейдерах
Dynasaur
Почему в 3Д-графике для поворота и переноса используются матричные операции, а в системах навигации кватернионы? Ведь они описывают одно и то же? Если мы моделируем движение самолёта в 3Д-сцене матаппарат должен быть тем же, что и в системе навигации?
Mike_666
Кватернионы описывают только повороты (хотя и делают это более эффективно).
А вот матричные операции, и не какие попало, а в однородных координатах - потому что таким образом можно унифицировано работать с поворотом, сдвигом и проективным преобразованием (то проецирует точки трёхмерного пространства на плоский экран).
И конечно же задачи моделирования и отображения - разные, и их можно решать с помощью разных инструментов.
Dynasaur
Да, верно. Матрицы ещё масштабировать позволяют, а самолёту в полёте масштабироваться не надо :-) Мог бы и сам догадаться :-) Спасибо
alsekond Автор
В системах навигации не разбираюсь. Про 3д графику могу сказать что ограничений на использование матриц нет. Движение самолёта можно смоделировать как матрицами так и кватернионами. Шейдерные языки заточены на использование матриц, но можно дописать функции работы с кватернионами. Например, файлы md5 используют кватернионы в описании костей модели.
alsekond Автор
Если же размышлять о системах навигации, то там вероятно свои задачи для которых выгодно использовать кватернионы. Могу предположить для экономии места на накопителях данных.
alsekond Автор
Вспомнил ещё, что кватернионы не страдают от Gimbal Lock и SLERP'ом можно плавно интерполировать их значения.