О трёхмерной графике в GMS2. Часть 1 из 2
Что такое вершинный буфер? Как создать трехмерный объект и отрисовать его на экран? Для чего нужен формат вершин и как с ним работает вертексный шейдер? Как работает буфер глубины и что такое борьба за глубину? Как это влияет на полупрозрачность и почему важен порядок отрисовки объектов на экран? Как посчитать координаты камеры и задать перспективу? Для чего нужны матрицы и как ими пользоваться? Что такое отсечение и зачем оно нужно?
Скриншоты в этом руководстве сняты не из GMS2, а из GMEdit для большей наглядности. В процессе объяснения я буду добавлять небольшие заметки о GMEdit, чтобы объяснить конструкции, непривычные для GMS2.
Первая заметка касается добавления проекта в GMEdit. Для этого я создаю проект в GMS2, добавляю один объект и его экземпляр в комнате. После сохранения проекта, открываю его в GMEdit.
Я хочу отобразить на экране куб. Для наглядности сначала нарисую его на бумаге.
На рисунке видно, что у куба 8 вершин, обозначенных буквами A, B, C, D, E, F, G, H. Опишу положение каждой вершины в событии Create.
s — это половина длины ребра куба. Оси XYZ расположены в центре куба и направлены вправо, вверх и вперёд соответственно. Координаты каждой точки описаны в виде массива со значениями [x, y, z]. Плюсы перед числами добавлены для удобства чтения, их можно опустить.
У куба шесть граней, каждая из которых состоит из двух треугольников. Чтобы отрисовать куб на экране, необходимо создать вершинный буфер, содержащий описание всех двенадцати треугольников. Начну с создания этого буфера.
Для начала нужно создать формат вершин, который будет использоваться при создании буфера. В строках 12–16 создаётся формат вершин. Это базовый формат для GMS2, я не буду его менять для упрощения читаемости гайда. Все данные вершин должны добавляться между вызовами vertex_format_begin() и vertex_format_end() . Функция возвращает ссылку на структуру формата, которую нельзя удалять до тех пор, пока она используется в вершинных буферах. После завершения работы с форматом его нужно удалить с помощью функции vertex_format_delete() .
Состав созданного формата в строках 13–15: Позиция вершины в пространстве, описываемая тремя значениями типа float (x, y, z); Текстурные координаты вершины (u, v), описываемые двумя значениями типа float ; Цвет вершины — uint .
Важен порядок добавления данных в формат — он должен совпадать с порядком в шейдере для корректной отрисовки на всех целевых платформах.
Далее, в строке 18 создаю вершинный буфер, а в строке 19 начинаю добавлять в него вершины. В этой функции необходимо указать созданный ранее формат вершин. В строке 21 завершаю добавление вершин, а в строке 22 переношу буфер в видеопамять для ускорения отрисовки. Пока буфер пуст, вершины будут добавлены далее, между строками 19 и 21.
Мне нужно добавить все шесть граней куба в вершинный буфер. На этом скриншоте грани добавляются с помощью функции plane_add . В неё передаются вершинный буфер, четыре точки, описывающие грань, и цвет грани. Для наглядности сделаю грани разного цвета. В строках 21–26 добавляются грани в следующем порядке: передняя (A, B, C, D), правая (D, C, G, H), задняя (H, G, F, E), левая (E, F, B, A), верхняя (B, F, G, C), нижняя (E, A, D, H). Вершины каждой грани добавляются в порядке: левая нижняя, левая верхняя, правая верхняя, правая нижняя. Можно свериться со схемой куба выше.
В строках 6–10 создаётся функция vert_add , с помощью которой буду добавлять вершины в буфер. В седьмой строке добавляются координаты в пространстве, в восьмой — текстурные координаты, а в девятой — цвет. Данные нужно добавлять в той же последовательности, что и в формате вершин. Куб будет без текстур, поэтому текстурные координаты всегда одинаковые.
Обращаю внимание, что во второй строке я добавил enum POS , описывающий координаты вершины в пространстве. В шестой строке тип POS используется для параметра, содержащего координаты вершины. Это позволяет записывать значения как v.x , v.y , v.z , а не использовать индексы массива ( v[0] , v[1] , v[2] ), что улучшает читаемость кода. Такие возможности предоставляет GMEdit.
В строках 12–17 с помощью vert_add добавляются треугольники, описывающие грань. Первый треугольник — v1, v2, v3; второй — v3, v4, v1. Например, для передней грани это треугольники ABC и CDA. Важно, что вершины треугольников располагаются по часовой стрелке для оптимизации отрисовки. Если включить функцию отсечения, треугольники, рисуемые против часовой стрелки, не будут отрисованы. Например, с задней стороны куба грани не будут видны, так как их треугольники будут рисоваться против часовой стрелки.
Покажу, как строки 2–10 выглядят в GMS2.
Тип POS в строке 10 закомментирован, а в строке 7 вместо v.x обращение к массиву через индексы: v[POS.x] , то есть v[0] . Код всегда будет написан так, а GMEdit отображает его более лаконично.
Вот текущие строки из события Create:
В строке 36 я удалил var , чтобы переменная vert_buffer была доступна в событии Draw.
Теперь с помощью vertex_submit(vert_buffer, pr_trianglelist, -1) в событии Draw отрисую куб на экране. Параметр pr_trianglelist указывает на отрисовку треугольников, а -1 — отсутствие текстуры.
На экране куб отображается в левом верхнем углу, что логично, так как его координаты заданы относительно начала координат. Чтобы сместить куб, нужно изменить мировую матрицу с помощью функции matrix_set .
В строке 4 создаётся матрица. Функции передаётся 9 параметров: перемещение, вращение и масштабирование для осей XYZ. Куб наклонён на 45 градусов по оси X для лучшей видимости, поворачивается по оси Y и смещён в координаты 300, 300 на экране. В строке 5 устанавливаю мировую матрицу. Она передаётся в шейдер вершин, где каждая вершина умножается на неё для вычисления новых координат на экране. В строке 7 возвращаю мировую матрицу к единичной, чтобы остальная графика отображалась корректно.
На анимации видно, что верхняя и нижняя грани перепутаны местами, а передняя и боковые грани пропадают. Это происходит из-за направления оси Y, которая на экране направлена вниз. Пропадающие грани можно вернуть, включив буфер глубины. Этот буфер хранит глубину каждого пикселя, и если глубина нового пикселя больше уже записанной, он не будет отрисован. Для этого в событие Create добавляю две функции:
gpu_set_zwriteenable — включает запись в буфер глубины; gpu_set_ztestenable — включает сравнение глубины пикселей с данными в буфере глубины.
Теперь куб отрисован корректно, а вращение происходит вокруг его центра.
На этом заканчивается первая часть статьи.
Спасибо, что дочитали статью до конца. Надеюсь, она вам понравилась, и вы узнали что-то новое. Если это так, поставьте лайк — возможно, тогда о ней узнает больше людей.
Сейчас я стараюсь выпускать по одной статье в неделю. Подписывайтесь на меня, чтобы не пропускать новые публикации.
Если вам понравилась эта статья, также рекомендую ознакомиться с другими материалами о программировании в GameMaker:
Спасибо за ваше внимание! Если вы считаете моё творчество полезным, то я буду очень рад если вы поддержите меня оформив подписку. Это поможет чаще выпускать статьи и разрабатывать игры.