Разработка для PlayStation
Часть 5. 3D-графика
Эта статья является частью цикла «Разработка для PlayStation», который выходил в «FPS» в 2015-2017 годах. Надеемся, что предыдущие части цикла скоро тоже будут доступны онлайн.
Вот мы и добрались до святая святых: трехмерной графики на PlayStation! Во время появления консоли (а это был, напомню, 1994 год) аппаратное ускорение 3Dрендеринга было самой настоящей инновацией – на ПК оно повсеместно появилось только годы спустя стараниями 3dfx и NVIDIA. Последняя, кстати, называет «первым GPU в мире» свой GeForce 256, вышедший в 1999 году, хотя это не более чем типичная маркетинговая ложь – у PSX был полноценный GPU, и для своего времени очень неплохой (достаточно быстрый для рендеринга игр уровня первых двух Quake). Кстати, во все анналы как первая полностью трехмерная игра вошла именно Quake (1996), но на PSX до нее были, например, полностью полигональные гонки Destruction Derby (1995), а также Battle Arena Toshinden (1995), один из первых 3Dфайтингов, и Jumping Flash! (1995), которая считается первым полностью трехмерным платформером. Конечно, они не достигли славы Quake, но среди фанатов PSX считаются бесспорными хитами.
Но вернемся к спецификациям. GPU PSX поддерживает отрисовку треугольников и четырехугольников, закрашенных сплошным цветом или заполненных текстурой. Также поддерживается освещение (плоское и интерполированное по Гуро), прозрачность и туман. Для выполнения геометрических преобразований PSX использует отдельный сопроцессор, GTE (Geometry Transformation Engine). Согласно заявленным спецификациям, PSX способен выводить до 180000 затекстуренных полигонов в секунду.
У 3Dрежима PSX есть и несколько недостатков. Самый главный из них – отсутствие перспективной коррекции текстур, изза чего происходит сильное искажение картинки на больших плоских поверхностях. К тому же, все вычисления на GTE делаются в числах с фиксированной запятой, а это зачастую приводит к заметному подергиванию 3Dобъектов на экране. Впрочем, подобные вещи стали уже своеобразной визитной карточкой олдскульных платформ, и старые игры без них смотрелись бы куда менее лампово...
Интересная особенность PSX – отсутствие аппаратной Zбуферизации. GPU умеет рисовать только 2Dпримитивы и ничего не знает о координате Z – задача по сортировке примитивов в зависимости от удаленности от камеры ложится на CPU. Сортировка делается очень остроумным методом, о котором я уже упоминал и теперь расскажу поподробнее.
Перебирать все треугольники и сравнивать их друг с другом – слишком дорого. Поэтому для ускорения сортировки используется нечто вроде хэштаблицы, где хэшфункцией выступает глубина примитива. Эта таблица называется таблицей порядка – Ordering Table (OT).
В основе OT лежит односвязный список, элементами которого выступают пакеты – то есть, команды видеопроцессору нарисовать тот или иной примитив. Пакет имеет 32битный заголовок, в 24 нижних битах которого хранится адрес следующего пакета (или 0xffffff, если пакет последний), а в 8 верхних – размер. Для хранения пакетов в оперативной памяти создается буфер фиксированного размера под некоторое максимальное количество дискретных значений глубины – обозначим его как N. Каждый пакет в буфере ссылается на предыдущий – таким образом, цепочка команд отрисовки хранится задом наперед.
Когда программа хочет нарисовать какойто примитив, она вычисляет его глубину и дискретизирует ее в диапазоне 0..N – получается индекс (i), по которому извлекается соответствующий элемент в буфере и вставляется новый пакет в список после этого элемента. Иными словами, пакет i ссылается на новый пакет, а тот – на пакет i+1. Затем GPU линейно обрабатывает все пакеты, начиная с последнего – последний пакет рисуется первым, поэтому они и хранятся в списке задом наперед. Получается сортировка сложности O(1).
Если какието примитивы будут иметь одинаковую глубину, возникнет коллизия – в данном случае новый пакет всегда будет помещаться после старого, и отрисован будет первым. Это и приводит к глюкам сортировки, которые порой можно видеть в некоторых играх на PSX, когда один объект перекрывает другой, хотя вроде и не должен. Чем больше значение N, тем точнее сортировка.
Ну, довольно теории, переходим к практике. Перед тем, как рендерить 3Dмодель, нужно ее создать и сохранить в особом формате – TMD. Подготовка моделей для PSX – достаточно трудоемкий процесс, но он отчасти облегчается путем использования специально написанных экспортеров для 3Dредакторов. Я лично являюсь пользователем Blender, поэтому меня интересовало, можно ли использовать его. Оказалось, что можно – существует экспортер для Blender 2.69 и выше, написанный все тем же Lameguy64. Ищите его по следующей ссылке: http://www.psxdev.net/forum/viewtopic.php?f=60&t=707&start=0 (если не сумели скачать, пишите мне на почту, я вам его отправлю).
Экспортер сохраняет модель в RSD – промежуточный формат, с которым работают утилиты PsyQ. Это простой текстовый формат с человекочитаемым описанием 3Dданных. Сама геометрия (вершины, нормали и треугольники) сохраняется в файл .ply, текстурные координаты и цвета вершин – в файл .mat. А файл .rsd просто ссылается на .ply и .mat. В этом же файле содержатся ссылки на TIMтекстуры, используемые моделью – имена файлов TIM соответствуют именам текстур в Blender, только с расширением .tim (в Blender, естественно, TIM не поддерживается, поэтому придется использовать другие графические форматы). Одна модель может содержать несколько текстур. При этом текстуры должны быть назначены моделям не через материалы, а в редакторе UV/Изображений. Будьте с этим внимательны, иначе ничего не заработает.
Итак, на выходе у вас должны быть 4 файла, лежащие в одной папке поскольку я для простоты смоделировал простой кубик, то назовем их cube.rsd, cube.ply, cube.mat и cube.tim. Теперь нам понадобится утилита RSDLINK, входящая в состав PsyQ. Она «компилирует» модель из текстового в бинарное представление – TMD, которое затем и загружается программой. Вызываем ее следующим образом:
rsdlink s 32.0 o cube.tmd cube.rsd
Параметр s задает коэффициент масштабирования модели. Рекомендуется начать с 32 и проверить программу, а затем увеличить это значение, если модель кажется слишком маленькой.
Поздравляю, вы создали свою первую 3Dмодель для PSX! Скопируйте ее вместе с текстурой в корень будущего диска и назовите CUBE.TMD и CUBE.TIM.
Переходим к коду. Объявляем переменные и инициализируем 3Dграфику:
Data timData, tmdData;
GsCOORDINATE2 camCoord2;
VECTOR camPos = {0};
SVECTOR camRot = {0};
VECTOR objPos = {0};
SVECTOR objRot = {0};
GsF_LIGHT light;
GsDOBJ2 obj;
int objectCount = 0;
// Здесь у нас GsInitGraph
// и другая стандартная инициализация
GsInit3D();
GsSetProjection(SCREEN_WIDTH/2);
Вторая функция задает расстояние от вьюпорта до плоскости проекции – фактически, меняет угол обзора камеры, поскольку размер плоскости проекции постоянен и зависит от разрешения экрана. Чем меньше это значение, тем больше угол обзора. В данном случае мы сделали его зависимым от ширины экрана. Значение по умолчанию – 1000.
Инициализируем координатную систему для камеры – по сути, под этим скрывается всего лишь создание единичной матрицы в мировом пространстве:
GsInitCoordinate2(WORLD, &camCoord2);
Задаем начальные позицию и поворот камеры:
camPos.vx = 0;
camPos.vy = 10;
camPos.vz = 150;
camRot.vy = 0;
camRot.vx = 0;
Задаем цвет окружения для освещения:
GsSetAmbient(ONE/4, ONE/4, ONE/4);
Задаем режим освещения (0 освещение без тумана, 1 с туманом):
GsSetLightMode(0);
Инициализируем источник света:
light.vx = 100;
light.vy = 100;
light.vz = 100;
light.r = 0xff;
light.g = 0xff;
light.b = 0xff;
GsSetFlatLight(0, &light);
Загружаем текстуру и модель:
readFile("\\CUBE.TIM;1", &timData);
loadTexture(&timData, &texture);
freeData(&timData);
readFile("\\CUBE.TMD;1", &tmdData);
objectCount += linkModel((u_long*)(tmdData.ptr), &obj);
obj.attribute = 0;
Функция linkModel присоединяет данные TMD к объекту GsDOBJ2. Она выглядит следующим образом:
int linkModel(u_long* tmd, GsDOBJ2* obj)
{
u_long* dop;
int i, numObj;
dop = tmd;
dop++;
GsMapModelingData(dop);
dop++;
numObj = *dop;
dop++;
for(i = 0; i < numObj; i++)
{
GsLinkObject4((u_long)dop, &obj[i], i);
obj[i].attribute = (1 << 6);
}
return (numObj);
}
Теперь мы можем рендерить. Главный цикл программы будет выглядеть так:
while(1)
{
currentBuffer = GsGetActiveBuff();
GsSetWorkBase((PACKET*)gpuPacketArea[currentBuffer]);
GsClearOt(0, 0, &myOT[currentBuffer]);
calcCamera();
GsSetFlatLight(0, &light);
objRot.vy += 10;
putObject(objPos, objRot, &obj);
FntFlush(1);
DrawSync(0);
VSync(0);
GsSwapDispBuff();
GsSortClear(50, 0, 50, &myOT[currentBuffer]);
GsDrawOt(&myOT[currentBuffer]);
}
Осталось определить две функции – calcCamera и putObject. Функция calcCamera вычисляет видовую матрицу для заданных позиции и поворота камеры и передает ее в GTE. Ее нужно вызвать перед тем, как рисовать объекты.
void calcCamera()
{
VECTOR vec;
GsVIEW2 view;
view.view = camCoord2;
view.super = WORLD;
RotMatrix(&camRot, &view.view);
ApplyMatrixLV(&view.view, &camPos, &vec);
TransMatrix(&view.view, &vec);
GsSetView2(&view);
}
Функция putObject рендерит заданный объект GsDOBJ2 с заданными позицией и поворотом:
void putObject(VECTOR pos, SVECTOR rot, GsDOBJ2* obj)
{
MATRIX lmtx, omtx;
GsCOORDINATE2 coord;
coord = camCoord2;
RotMatrix(&rot, &omtx);
TransMatrix(&omtx, &pos);
CompMatrixLV(&Camera.coord2.coord, &omtx, &coord.coord);
coord.flg = 0;
obj>coord2 = &coord;
GsGetLws(obj>coord2, &lmtx, &omtx);
GsSetLightMatrix(&lmtx);
GsSetLsMatrix(&omtx);
GsSortObject4(obj, &myOT[currentBuffer], 14OT_LENGTH, getScratchAddr(0));
}
Если все сделано правильно, вы должны увидеть в окне эмулятора вот такой вращающийся кубик. Ура!
Напоследок напомню, что существует такой сайт, как http://www.psxdev.net там очень много полезной актуальной информации по разработке под PSX, есть активное сообщество. Если возникли вопросы, не стесняйтесь, пишите мне на почту: gecko0307@gmail.com, постараюсь помочь по мере своих возможностей.