Вступление

Компьютерная графика всегда была для меня сложной темой. Каждый раз, читая об этом, я все больше и больше запутывался. Это трудная для понимания концепция, и огромное количество разных названий для одного и того же (и названий, которые не имеют никакого смысла) просто сводили меня с ума. Однако с тех пор, как я получил PICO-8, я хотел создать на нем средство 3D-рендеринга без каких-либо абстракций, с простым API для рисования на экране. Вот так выглядит готовый продукт!



Pico Engine от MatheusMortatti
Простой 3D-движок в PICO-8. Доступно для игры онлайн matheusmortatti.itch.io



Итак, около двух месяцев назад я получил образец тележки PICO-8, который я нашел в PICOZINE (это здорово!). В нем были рассмотрены основы перспективного рендеринга каркасного куба на экране:

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

Вот тогда и начались мои поиски понимания конвейера 3D-рендеринга! А теперь я попытаюсь объяснить вам все концепции, которые я изучил (и математику, которая к ним пришла), а также то, где я потерпел неудачу в процессе. Я сосредоточил свои усилия на рендеринге треугольников, но большую часть процесса можно применить к другим типам фигур (вам нужно будет изменить способ рисования фигуры на экране, в основном).

Преобразования вершин

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

  1. Создайте 3D-форму (т.е. набор треугольников). Точки в пространстве должны быть объявлены в системе координат фигуры, в пространстве объекта (например, точка (0,0,0) может быть в середине фигуры).
  2. Переведите и поверните фигуру в систему координат вашего мира (Мировое пространство).
  3. Переведите и поверните фигуру в пространство камеры. По сути, камера теперь является вашей исходной точкой (0,0,0), и она указывает на ось -Z (по соглашению), а все другие фигуры необходимо переместить в эту систему координат, чтобы вы знали, где они связаны с камерой. На этом шаге вы знаете, что следует и не следует визуализировать, например объекты, которые находятся за камерой (т.е. они находятся где-то на положительной оси Z).
  4. Спроецируйте 3D-точки из последнего шага в 2D-точки в системе координат экрана. Если вы видите изображение выше, у нас есть однородное пространство клипа, которое представляет собой способ представить все точки вашей сцены, где диапазон [-1,1] для x и y - это то, что может быть видно камерой. Затем мы преобразуем этот диапазон в фактический размер разрешения, так что у вас будет 0 <= x < ResolutionX и 0 <= y < ResolutionY.
  5. Заполните треугольник, используя технику растеризации.

Это шаги, которые мне нужно было изучить, чтобы получить кучу трехмерных точек и преобразовать их в точки на экране PICO-8. Я собираюсь разбить это на несколько шагов и попытаться связать их все!

Форма

Это довольно просто, я просто создал структуру в коде, чтобы сохранить кучу треугольников и их цвета. Это выглядит так: фигура содержит набор треугольников, а треугольник содержит 3 вершины и цвет. Каждая вершина имеет координаты x, y и z. Вот пример:

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

От объекта к мировому пространству

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



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

Пространство камеры

Для меня это было непросто. Мне потребовалось время, чтобы понять, как преобразовать все в пространство камеры, потому что есть несколько способов сделать это. В итоге я использовал метод камеры FPS.

То, что мы используем для преобразования точки из мирового пространства в пространство камеры, мы называем матрицей обзора. Это матрица, которая содержит информацию об ориентации камеры (куда она направлена) и положении. Он определяется с использованием значений Yaw и Pitch (вращение вокруг оси Y и оси X соответственно) и точки Глаза (где камера ). Теоретически процесс очень прост: перевести все в новое начало (положение камеры), а затем повернуть все вокруг в зависимости от ориентации камеры, вот что делает эта матрица. Однако теперь нам нужно заняться математикой.

x = {cos(Pitch), 0, -sin(Pitch)}
y = {sin(Yaw)*sin(Pitch), cos(Pitch), cos(Yaw)*sin(Pitch)}
z = {sin(Yaw)*cos(Pitch), -sin(Pitch), cos(Yaw)}
FPSViewMatrix[4][4] = {
    {x[1],y[1],z[1]}
    {x[2],y[2],z[2]}
    {x[3],y[3],z[3]}
    {-dot(x, eye), -dot(y, eye), -dot(z, eye)}
}

С помощью этого вычисления матрицы просмотра мы можем умножить ее на нашу точку (x, y, z).

Одна вещь, которая меня всегда смущала, это как, черт возьми, я собираюсь умножать матрицу 4x4 на точку 1x3? Какое 4-е значение? Ответ: 1. Четвертое значение называется координатой w, и что оно означает, для меня до сих пор остается загадкой, но вы хотите, чтобы оно было равно 1 (возможно, скажите мне, что это значит в твиттере?).

Имейте в виду, что эта матрица была объявлена ​​в соответствии с соглашением Row Major. Это означает, что наша точка - это 1x3 (1x4 с координатой w), и мы собираемся умножить ее на матрицу 4x4, поэтому нам нужно сделать это в порядке V * M, где V - наша точка, а M - матрица.

После этого умножения у нас теперь есть сдвинутая и повернутая точка в пространстве камеры, и теперь мы готовы проецировать эту точку на экран и рисовать объекты на экране!

Сгладить вещи

Последний шаг в этом процессе - спроецировать точки, которые мы имеем в пространстве камеры, на наше оконное пространство! Для этого у нас есть очень простая формула, которая связывает наши x, y и z, чтобы получить экран x и y:

Screen X = - (resX/2) * (x / z) + (resX/2)
Screen Y = - (resY/2) * (y / z) + (resY/2)

Это уравнение означает, что мы преобразуем (x, y, z) в 2D-координаты (x, y) с шагом projection в нашем конвейере, разделив x и y на -z, а затем применив преобразование области просмотра путем умножения этого значения на каждый размер разрешения, разделенный на два, в результате чего видимые точки находятся в диапазоне [-resX/2, resX/2] и [-resY/2, resY/2]. Наконец, мы исправляем значения, чтобы они находились в диапазоне [0, resX] и [0, resY].

Знак минус присутствует, потому что по нашему соглашению все видимые с камеры точки должны находиться на отрицательной оси Z. В случае PICO-8 resX и resY оба равны 127, потому что координаты пикселей изменяются от 0 до 127.

На самом деле это очень упрощенная версия самого шага. Если бы у нас было другое соотношение сторон, чем 1: 1, и изменяющееся поле зрения, нам пришлось бы немного изменить эти вычисления.

Еще несколько шагов

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

Заполнить его!

Здесь мы заполняем треугольник цветом. Я сделал в режиме Scanline, заполнив треугольник линиями снизу вверх.

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

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



Подсвети это!

Пришло время применить к нашей сцене немного света. Я сделал это простым способом: просто вычислил угол между нормалью каждого треугольника и вектором, представляющим направление света. Это создает Directional Light, который используется для создания глобального освещения вашей сцены, такого как солнце. Он не использует ничего, кроме ориентации треугольника по отношению к свету, чтобы вычислить новый цвет вашего треугольника. Конечно, это огромное упрощение, это намного сложнее в более надежном 3D-движке.

В PICO-8 вам нужно полагаться на смену палитры, чтобы имитировать свет. Это делается путем создания таблицы палитры, которая содержит для данного цвета все его значения в зависимости от желаемого уровня освещенности. Позвольте мне показать вам, что я имею в виду.

На рисунке выше показан пример уровней освещенности для каждого из цветов PICO-8. Как видите, каждый столбец представляет собой уровень освещенности, а первый столбец содержит исходные цвета. Эту специфическую палитру света я получил из следующей статьи, написанной Якубом Василевски. На нем он объясняет, как он сделал безумное освещение в реальном времени в PICO-8 в 4 частях, и в первой он подробно рассказывает о световых палитрах, вы должны прочитать это, чтобы узнать об этом больше :).



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

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

С векторами v и u теперь мы можем вычислить перекрестное произведение между ними, чтобы получить желаемую нормаль, а затем нормализовать его.

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

cos(angle) = ( a . b ) / ( |a|*|b| )

То есть косинус угла - это скалярное произведение двух векторов, деленное на произведение их длин. С этим значением нам просто нужно изменить его диапазон с (-1,1) на (1, размер палитры), и на этом все готово! Я просто добавил 1 к значению и разделил его на 2, чтобы получить диапазон (0,1). Строка angle = abs(angle) - это просто мера безопасности, потому что в Lua вы можете иметь значение -0, и это все портит.

Заключение

Есть МНОГО шагов, которые я не описал ни здесь, ни в моем движке. Я пропустил несколько формальностей, чтобы сделать это и мне было что показать людям. Если вы хотите создать свой собственный 3D-движок в PICO-8, обязательно поищите больше источников, чтобы узнать больше о процессе, как и я!

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

Спасибо за внимание,

Матеус Мортатти.